')
+ .append($('')
+ .html($(elem).html())) },
+ // XXX expperimental...
+ json: function(context, elem){ return $('')
+ .html($(elem).text()
+ // remove JS comments...
+ .replace(/\s*\/\/.*$|\s*\/\*(.|[\n\r])*?\*\/\s*/mg, '')) },
+
+ // XXX
+ nl2br: function(context, elem){
+ return $('').html($(elem).html().replace(/\n/g, '
\n')) },
+
+ wikiword: function(context, elem){
+ return $('
')
+ .html(setWikiWords($(elem).html(), true, this.__include_marker__)) },
+ // XXX need to remove all on* event handlers...
+ noscript: function(context, elem){
+ return $(elem)
+ // remove script tags...
+ .find('script')
+ .remove()
+ .end()
+ // remove js links...
+ .find('[href]')
+ .filter(function(i, e){ return /javascript:/i.test($(e).attr('href')) })
+ .attr('href', '#')
+ .end()
+ .end()
+ // remove event handlers...
+ // XXX .off() will not work here as we need to remove on* handlers...
+ },
+
+ // XXX move this to a plugin...
+ markdown: function(context, elem){
+ var converter = new showdown.Converter({
+ strikethrough: true,
+ tables: true,
+ tasklists: true,
+ })
+
+ return $('')
+ .html(converter.makeHtml($(elem).html()))
+ // XXX add click handling to checkboxes...
+ .find('[checked]')
+ .parent()
+ .addClass('checked')
+ .end()
+ .end()
+ },
+ },
+
+
+ // Post-filters...
+ //
+ // These are run on the final page.
+ //
+ // The main goal is to setup editors and other active stuff that the
+ // user should not have direct access to, but that should be
+ // configurable per instance...
+ //
+ // for tech and other details see .filter
+ //
+ post_filter: {
+ noscript: function(context, elem){
+ // XXX
+ return elem
+ },
+
+ // Setup the page title and .title element...
+ //
+ // Use the text from:
+ // 1) set it H1 if it is the first tag in .text
+ // 2) set it to .location
+ //
+ title: function(context, elem){
+ elem = $(elem)
+ var title = elem.find('.text h1').first()
+
+ // show first H1 as title...
+ if(elem.find('.text').text().trim().indexOf(title.text().trim()) == 0){
+ title.detach()
+ elem.find('.title').html(title.html())
+ $('title').html(title.text())
+
+ // show location...
+ } else {
+ $('title').text(context.location)
+ }
+
+ return elem
+ },
+ // XXX this needs save/reload...
+ editor: function(context, elem){
+ // XXX title
+ // - on focus set it to .title
+ // XXX text
+ // XXX raw
+ // XXX checkbox
+
+ return elem
+ },
+ },
+
+
+ // Parsing:
+ // 1) expand macros
+ // 2) apply filters
+ // 3) merge and parse included pages:
+ // 1) expand macros
+ // 2) apply filters
+ // 4) fill slots
+ // 5) expand post-macros
+ //
+ // NOTE: stage 4 parsing is executed on the final merged page only
+ // once. i.e. it is not performed on the included pages.
+ // NOTE: included pages are parsed in their own context.
+ // NOTE: slots are parsed in the context of their containing page
+ // and not in the location they are being placed.
+ //
+ // XXX support quoted text...
+ // XXX need to quote regexp chars of .__include_marker__...
+ // XXX include recursion is detected but path recursion is not at
+ // this point...
+ // e.g. the folowing paths resolve to the same page:
+ // /SomePage
+ // /SomePage/SomePage
+ // or any path matching:
+ // /\/(SomePage\/)+/
+ parse: function(context, text, state, skip_post, pattern){
+ var that = this
+
+ state = state || {}
+ state.filters = state.filters || []
+ //state.slots = state.slots || {}
+ state.include = state.include || []
+ state.seen = state.seen || []
+
+ //pattern = pattern || RegExp('@([a-zA-Z-_]+)\\(([^)]*)\\)', 'mg')
+ pattern = pattern || RegExp.apply(null, this.__macro__pattern__)
+
+ // XXX need to quote regexp chars...
+ var include_marker = RegExp(this.__include_marker__, 'g')
+
+ var parsed = typeof(text) == typeof('str') ?
+ $('').html(text)
+ : text
+
+ var _parseText = function(context, text, macro){
+ return text.replace(pattern, function(match){
+ // quoted macro...
+ if(match[0] == '\\' && macro['*'] == null){
+ return match.slice(1)
+ //return match
+ }
+
+ // XXX parse match...
+ var d = match.match(/@([a-zA-Z-_:]*)\(([^)]*)\)/)
+
+ var name = d[1]
+
+ if(name in macro || '*' in macro){
+ var elem = $('<'+name+'/>')
+
+ name = name in macro ? name : '*'
+
+ // format positional args....
+ var a = d[2]
+ .split(/((['"]).*?\2)|\s+/g)
+ // cleanup...
+ .filter(function(e){ return e && e != '' && !/^['"]$/.test(e)})
+ // remove quotes...
+ .map(function(e){ return /^(['"]).*\1$/.test(e) ? e.slice(1, -1) : e })
+
+ // add the attrs to the element...
+ name != '*'
+ && a.forEach(function(e, i){
+ var k = ((macro[name] || {}).macro_args || [])[i]
+ k && elem.attr(k, e)
+ })
+
+ // call macro...
+ var res = macro[name]
+ .call(that, context, elem, state,
+ function(elem, c){
+ return _parse(c || context, elem, macro) },
+ match)
+
+ return res instanceof jQuery ?
+ // merge html of the returned set of elements...
+ res.map(function(i, e){ return e.outerHTML })
+ .toArray()
+ .join('\n')
+ : typeof(res) != typeof('str') ? res.outerHTML
+ : res
+ }
+
+ return match
+ })
+ }
+ // NOTE: this modifies parsed in-place...
+ var _parse = function(context, parsed, macro){
+ $(parsed).contents().each(function(_, e){
+ // #text / comment node -> parse the @... macros...
+ if(e.nodeType == e.TEXT_NODE || e.nodeType == e.COMMENT_NODE){
+ // get actual element content...
+ var text = $('').append($(e).clone()).html()
+
+ // conditional comment...
+ if(e.nodeType == e.COMMENT_NODE
+ && /^$/.test(text)){
+ text = text
+ .replace(/^$/, '')
+ }
+
+ $(e).replaceWith(_parseText(context, text, macro))
+
+ // node -> html-style + attrs...
+ } else {
+ var name = e.nodeName.toLowerCase()
+
+ // parse attr values...
+ for(var i=0; i < e.attributes.length; i++){
+ var attr = e.attributes[i]
+
+ attr.value = _parseText(context, attr.value, macro)
+ }
+
+ // macro match -> call macro...
+ if(name in macro){
+ $(e).replaceWith(macro[name]
+ .call(that, context, e, state,
+ function(elem, c){
+ return _parse(c || context, elem, macro) }))
+
+ // normal tag -> sub-tree...
+ } else {
+ _parse(context, e, macro)
+ }
+ }
+ })
+
+ return parsed
+ }
+ var _filter = function(lst, filters){
+ lst
+ // unique -- leave last occurance..
+ .filter(function(k, i, lst){
+ return k[0] != '-'
+ // filter dupplicates...
+ && lst.slice(i+1).indexOf(k) == -1
+ // filter disabled...
+ && lst.slice(0, i).indexOf('-' + k) == -1
+ })
+ // unique -- leave first occurance..
+ //.filter(function(k, i, lst){ return lst.slice(0, i).indexOf(k) == -1 })
+ // apply the filters...
+ .forEach(function(f){
+ var k = f
+ // get filter aliases...
+ var seen = []
+ while(typeof(k) == typeof('str') && seen.indexOf(k) == -1){
+ seen.push(k)
+ k = filters[k]
+ }
+ // could not find the filter...
+ if(!k){
+ //console.warn('Unknown filter:', f)
+ return
+ }
+ // use the filter...
+ parsed = k.call(that, context, parsed)
+ })
+ }
+
+ // macro stage...
+ _parse(context, parsed, this.macro)
+
+ // filter stage...
+ _filter(state.filters.concat(this.__filters__), this.filter)
+
+ // merge includes...
+ parsed
+ .html(parsed.html().replace(include_marker, function(){
+ var page = state.include.shift()
+ var elem = $(page.shift())
+ page = page.pop()
+ var isolated = elem.attr('isolated') == 'true'
+
+ var seen = state.seen.slice()
+ if(seen.indexOf(page.path) >= 0){
+ return elem.html()
+ }
+ seen.push(page.path)
+
+ return page.map(function(page){
+ return $('
')
+ .append(elem
+ .clone()
+ .attr('src', page.path)
+ .append(that
+ .parse(page,
+ page.raw,
+ {
+ //slots: !isolated ? state.slots : {},
+ templates: state.templates,
+ seen: seen,
+ },
+ !isolated)))
+ //true)))
+ .html()
+ }).join('\n')
+ }))
+
+ // post processing...
+ if(!skip_post){
+ // fill slots...
+ // XXX need to prevent this from processing slots in editable
+ // elements...
+ slots = {}
+ // get slots...
+ parsed.find('slot')
+ .each(function(i, e){
+ e = $(e)
+
+ // XXX not sure about this...
+ // ...check if it prevents correct slot parsing
+ // within an isolated include...
+ if(e.parents('[isolated="true"]').length > 0){
+ return
+ }
+
+ var n = e.attr('name')
+
+ n in slots && e.detach()
+
+ slots[n] = e
+ })
+ // place slots...
+ parsed.find('slot')
+ .each(function(i, e){
+ e = $(e)
+
+ // XXX not sure about this...
+ // ...check if it prevents correct slot parsing
+ // within an isolated include...
+ if(e.parents('[isolated="true"]').length > 0){
+ return
+ }
+
+ var n = e.attr('name')
+
+ e.replaceWith(slots[n])
+ })
+
+ // post-macro...
+ // XXX for some odd reason this clears the backslash from
+ // quoted macros in raw fields...
+ //this.post_macro
+ // && _parse(context, parsed, this.post_macro)
+ }
+
+ // post-filter stage...
+ // XXX get list from context.config...
+ _filter(this.__post_filters__, this.post_filter)
+
+ // XXX shuld we get rid of the root span???
+ return parsed.contents()
+ },
+}
+
+
+
+/**********************************************************************
+* vim:set ts=4 sw=4 : */ return module })
diff --git a/pwiki.js b/pwiki.js
index db6bedc..43721ba 100755
--- a/pwiki.js
+++ b/pwiki.js
@@ -11,6 +11,8 @@ var object = require('lib/object')
var actions = require('lib/actions')
var features = require('lib/features')
+var macro = require('macro')
+
/*********************************************************************/
@@ -81,17 +83,19 @@ module.path2re = function(path){
/*********************************************************************/
-// base pWiki object...
-var pWiki =
-module.pWiki = object.makeConstructor('pWiki', actions.MetaActions)
-
// pWiki featureset...
var pWikiFeatures =
module.pWikiFeatures = new features.FeatureSet()
+/*
+// base pWiki object...
+var pWiki =
+module.pWiki = object.makeConstructor('pWiki', actions.MetaActions)
+
// base instance constructor...
pWikiFeatures.__actions__ =
function(){ return actions.Actions(pWiki()) }
+//*/
@@ -140,11 +144,13 @@ module.pWikiData = {
// XXX should this overwrite or expand???
// XXX should from be pattern compatible???
data: function(path, value){
+ // get the data...
if(value == null){
return this.__data ?
- JSON.parse(JSON.stringify(this.__data[path]))
+ JSON.parse(JSON.stringify(this.__data[path] || {}))
: null
+ // set the data...
} else {
this.__data = this.__data || {}
this.__data[path] = JSON.parse(JSON.stringify(value))
@@ -189,9 +195,10 @@ module.pWikiData = {
/*********************************************************************/
-// XXX need a startup sequence...
-var pWikiPageActions =
-module.pWikiPageActions = actions.Actions({
+// Base pWiki page API...
+//
+var pWikiBase =
+module.pWikiBase = actions.Actions({
config: {
'home-page': 'WikiHome',
'default-page': 'EmptyPage',
@@ -205,6 +212,11 @@ module.pWikiPageActions = actions.Actions({
'post-acquesition-order': [],
'order-unsorted-first': false,
+
+ // sorting...
+ 'default-sort-methods': [
+ 'path',
+ ],
},
// pWikiData...
@@ -383,7 +395,10 @@ module.pWikiPageActions = actions.Actions({
var d = this.data()
// get...
if(arguments.length == 1){
- return d[name]
+ return d[name] === undefined ?
+ // force returning undefined...
+ actions.UNDEFINED
+ : d[name]
// clear...
} else if(value === undefined){
@@ -393,6 +408,8 @@ module.pWikiPageActions = actions.Actions({
} else {
d[name] = value
}
+
+ // write the data...
// XXX is it good to write the whole thing???
this.data(d)
}],
@@ -648,7 +665,6 @@ module.pWikiPageActions = actions.Actions({
}
}],
- __default_sort_methods__: ['path'],
__sort_methods__: {
title: function(a, b){
return a.page.title() < b.page.title() ? -1
@@ -718,8 +734,7 @@ module.pWikiPageActions = actions.Actions({
: [].slice.call(arguments)
res.__order_by = methods = methods.length == 0 ?
- (this.__default_sort_methods__
- || pWikiPage.__default_sort_methods__)
+ (this.config['default-sort-methods'] || ['path'])
: methods
res.update()
@@ -754,11 +769,11 @@ module.pWikiPageActions = actions.Actions({
var reverse = false
var sort_methods = this.__sort_methods__
- || pWikiPage.__sort_methods__
+ || pWikiBase.__sort_methods__
var methods = (this.__order_by
- || this.__default_sort_methods__
- || pWikiPage.__default_sort_methods__)
+ || this.config['default-sort-methods']
+ || ['path'])
.map(function(m){
var reversed = m[0] == '-'
m = reversed ? m.slice(1) : m
@@ -813,14 +828,72 @@ module.pWikiPageActions = actions.Actions({
}],
})
-var pWikiPage = pWikiFeatures.Feature({
- title: '',
- tag: 'page',
- actions: pWikiPageActions,
+// Basic data sort-hands...
+//
+var pWikiContent =
+module.pWikiContent = actions.Actions(pWikiBase, {
+ config: {
+ },
+
+ raw: ['Page/',
+ function(value){
+ return arguments.length == 0 ?
+ (this.attr('text') || '')
+ : this.attr('text', value) }],
+
+ checked: ['Page/',
+ function(value){
+ return arguments.length == 0 ?
+ !!this.attr('checked')
+ : this.attr('checked', value || undefined) }],
})
+// Data processing and macros...
+//
+var pWikiMacros =
+module.pWikiMacros = actions.Actions(pWikiContent, {
+ __macro_parser__: macro,
+
+ config: {
+ },
+
+ text: ['Page/',
+ function(value){
+ return arguments.length == 0 ?
+ (this.title() == 'raw' ?
+ (this.raw() || '')
+ : pWikiMacros.__macro_parser__.parse(this, this.raw()))
+ : this.raw(value) }],
+ code: ['Page/',
+ function(value){
+ return arguments.length == 0 ?
+ this.text().text()
+ // XXX should we un-encode here???
+ : this.text(value) }],
+
+ // XXX
+ links: ['Page/',
+ function(){
+ }],
+})
+
+
+// pWiki Page...
+//
+// NOTE: looks like multiple inheritance, feels like multiple inheritance
+// but sadly is not multiple inheritance...
+// ...though, functionally, this is 90% there, about as far as we
+// can get using native JS lookup mechanisms, or at least the
+// farthest I've pushed it so far...
+var pWikiPage =
+module.pWikiPage = actions.mix(
+ pWikiBase,
+ pWikiContent,
+ pWikiMacros)
+
+
/*********************************************************************/
@@ -922,7 +995,7 @@ module._test = function(){
var wiki = Object.create(pWikiData)
wiki.__data = Object.create(module._test_data)
- var w = pWikiPageActions.clone()
+ var w = pWikiPage.clone()
w.wiki = wiki
return w
}