split off the macros, still not ready but already working...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2016-09-27 04:44:15 +03:00
parent 56065a128c
commit b5e0355886
2 changed files with 775 additions and 20 deletions

682
macro.js Executable file
View File

@ -0,0 +1,682 @@
/**********************************************************************
*
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/
/*********************************************************************/
function Macro(doc, args, func){
func.doc = doc
func.macro_args = args
return func
}
// XXX should inline macros support named args???
var macro =
module = {
__include_marker__: '{{{INCLUDE-MARKER}}}',
// Abstract macro syntax:
// Inline macro:
// @macro(arg ..)
//
// HTML-like:
// <macro arg=value ../>
//
// HTML-like with body:
// <macro arg=value ..>
// ..text..
// </macro>
//
// XXX should inline macros support named args???
__macro__pattern__:
[[
// @macro(arg ..)
'\\\\?@([a-zA-Z-_]+)\\(([^)]*)\\)'
].join('|'), 'mg'],
// default filters...
//
// NOTE: these are added AFTER the user defined filters...
__filters__: [
'wikiword',
'noscript',
],
__post_filters__: [
//'noscript',
'title',
'editor',
],
// Macros...
//
// XXX add support for sort and reverse attrs in all relavant macros
// (see: macro for details)
macro: {
"pwiki-comment": Macro('hide in pWiki',
[],
function(context, elem, state){
return ''
}),
now: Macro('Create a now id',
[],
function(context, elem, state){ return ''+Date.now() }),
// select filter to post-process text...
filter: Macro('Filter to post-process text',
['name'],
function(context, elem, state){
var filter = $(elem).attr('name')
filter[0] == '-' ?
// disabled -- keep at head of list...
state.filters.unshift(filter)
// normal -- tail...
: state.filters.push(filter)
return ''
}),
// include page/slot...
//
// NOTE: this will render the page in the caller's context.
// NOTE: included pages are rendered completely independently
// from the including page.
include: Macro('Include page',
['src', 'isolated', 'text'],
function(context, elem, state){
var path = $(elem).attr('src')
// get and prepare the included page...
state.include
.push([elem, context.get(path)])
// return the marker...
return this.__include_marker__
}),
// NOTE: this is similar to include, the difference is that this
// includes the page source to the current context while
// include works in an isolated context
source: Macro('Include page source (without parsing)',
['src'],
function(context, elem, state){
var path = $(elem).attr('src')
return context.get(path)
.map(function(page){ return page.raw })
.join('\n')
}),
quote: Macro('Include quoted page source (without parsing)',
['src'],
function(context, elem, state){
elem = $(elem)
var path = elem.attr('src')
return $(context.get(path)
.map(function(page){
return elem
.clone()
.attr('src', page.path)
.text(page.raw)[0]
}))
}),
/*
// fill/define slot (stage 1)...
//
// XXX which should have priority the arg text or the content???
_slot: Macro('Define/fill slot',
['name', 'text'],
function(context, elem, state, parse){
var name = $(elem).attr('name')
// XXX
text = $(elem).html()
text = text == '' ? $(elem).attr('text') : text
text = this.parse(context, text, state, true)
//text = parse(elem)
if(state.slots[name] == null){
state.slots[name] = text
// return a slot macro parsable by stage 2...
//return '<_slot name="'+name+'">'+ text +'</slot>'
return elem
} else if(name in state.slots){
state.slots[name] = text
return ''
}
}),
//*/
// convert @ macro to html-like + parse content...
slot: Macro('Define/fill slot',
['name', 'text'],
function(context, elem, state, parse){
elem = $(elem)
var name = elem.attr('name')
// XXX
text = elem.html()
text = text.trim() == '' ?
elem.html(elem.attr('text') || '').html()
: text
text = parse(elem)
elem.attr('text', null)
//elem.html(text)
return elem
}),
// XXX revise macro definition rules -- see inside...
// XXX do we need macro namespaces or context isolation (for inculdes)???
macro: Macro('Define/fill macro',
['name', 'src', 'sort'],
function(context, elem, state, parse){
elem = $(elem)
var name = elem.attr('name')
var path = elem.attr('src')
var sort = elem.attr('sort')
state.templates = state.templates || {}
// get named macro...
if(name){
// XXX not sure which definition rules to use for macros...
// - first define -- implemented now
// - last define -- as in slots
// - first contenr -- original
//if(elem.html().trim() != ''){
if(elem.html().trim() != ''
// do not redefine...
&& state.templates[name] == null){
state.templates[name] = elem.clone()
} else if(name in state.templates) {
elem = state.templates[name]
}
}
// fill macro...
if(path){
var pages = context.get(path)
// no matching pages -- show the else block or nothing...
if(pages.length == 0){
var e = elem
.find('else').first().clone()
.attr('src', path)
parse(e, context)
return e
}
// see if we need to overload attrs...
sort = sort == null ? (elem.attr('sort') || '') : sort
sort = sort
.split(/\s+/g)
.filter(function(e){ return e && e != '' })
// do the sorting...
pages = sort.length > 0 ? pages.sort(sort) : pages
// fill with pages...
elem = elem.clone()
.find('else')
.remove()
.end()
return $(pages
.map(function(page){
var e = elem.clone()
.attr('src', page.path)
parse(e, page)
return e[0]
}))
}
return ''
})
},
// Post macros...
//
// XXX this is disabled for now, see end of .parse(..)
post_macro: {
'*': Macro('cleanup...',
[],
function(context, elem, state, parse, match){
if(match != null){
return match[0] == '\\' ? match.slice(1) : match
}
return elem
}),
/*
_slot: Macro('',
['name'],
function(context, elem, state){
var name = $(elem).attr('name')
if(state.slots[name] == null){
return $(elem).html()
} else if(name in state.slots){
return state.slots[name]
}
}),
//*/
/*
// XXX rename to post-include and post-quote
'page-text': Macro('',
['src'],
function(context, elem, state){
elem = $(elem)
return elem.html(context.get(elem.attr('src')).text)
}),
'page-raw': Macro('',
['src'],
function(context, elem, state){
elem = $(elem)
return elem.text(context.get(elem.attr('src')).text)
}),
//*/
},
// Filters...
//
// Signature:
// filter(text) -> html
//
filter: {
default: 'html',
html: function(context, elem){ return $(elem) },
text: function(context, elem){ return $('<span>')
.append($('<pre>')
.html($(elem).html())) },
// XXX expperimental...
json: function(context, elem){ return $('<span>')
.html($(elem).text()
// remove JS comments...
.replace(/\s*\/\/.*$|\s*\/\*(.|[\n\r])*?\*\/\s*/mg, '')) },
// XXX
nl2br: function(context, elem){
return $('<div>').html($(elem).html().replace(/\n/g, '<br>\n')) },
wikiword: function(context, elem){
return $('<span>')
.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 $('<span>')
.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') ?
$('<span>').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 = $('<div>').append($(e).clone()).html()
// conditional comment...
if(e.nodeType == e.COMMENT_NODE
&& /^<!--\s*\[pWiki\[(.|\n)*\]\]\s*-->$/.test(text)){
text = text
.replace(/^<!--\s*\[pWiki\[/, '')
.replace(/\]\]\s*-->$/, '')
}
$(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 $('<div>')
.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 })

113
pwiki.js
View File

@ -11,6 +11,8 @@ var object = require('lib/object')
var actions = require('lib/actions') var actions = require('lib/actions')
var features = require('lib/features') 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... // pWiki featureset...
var pWikiFeatures = var pWikiFeatures =
module.pWikiFeatures = new features.FeatureSet() module.pWikiFeatures = new features.FeatureSet()
/*
// base pWiki object...
var pWiki =
module.pWiki = object.makeConstructor('pWiki', actions.MetaActions)
// base instance constructor... // base instance constructor...
pWikiFeatures.__actions__ = pWikiFeatures.__actions__ =
function(){ return actions.Actions(pWiki()) } function(){ return actions.Actions(pWiki()) }
//*/
@ -140,11 +144,13 @@ module.pWikiData = {
// XXX should this overwrite or expand??? // XXX should this overwrite or expand???
// XXX should from be pattern compatible??? // XXX should from be pattern compatible???
data: function(path, value){ data: function(path, value){
// get the data...
if(value == null){ if(value == null){
return this.__data ? return this.__data ?
JSON.parse(JSON.stringify(this.__data[path])) JSON.parse(JSON.stringify(this.__data[path] || {}))
: null : null
// set the data...
} else { } else {
this.__data = this.__data || {} this.__data = this.__data || {}
this.__data[path] = JSON.parse(JSON.stringify(value)) this.__data[path] = JSON.parse(JSON.stringify(value))
@ -189,9 +195,10 @@ module.pWikiData = {
/*********************************************************************/ /*********************************************************************/
// XXX need a startup sequence... // Base pWiki page API...
var pWikiPageActions = //
module.pWikiPageActions = actions.Actions({ var pWikiBase =
module.pWikiBase = actions.Actions({
config: { config: {
'home-page': 'WikiHome', 'home-page': 'WikiHome',
'default-page': 'EmptyPage', 'default-page': 'EmptyPage',
@ -205,6 +212,11 @@ module.pWikiPageActions = actions.Actions({
'post-acquesition-order': [], 'post-acquesition-order': [],
'order-unsorted-first': false, 'order-unsorted-first': false,
// sorting...
'default-sort-methods': [
'path',
],
}, },
// pWikiData... // pWikiData...
@ -383,7 +395,10 @@ module.pWikiPageActions = actions.Actions({
var d = this.data() var d = this.data()
// get... // get...
if(arguments.length == 1){ if(arguments.length == 1){
return d[name] return d[name] === undefined ?
// force returning undefined...
actions.UNDEFINED
: d[name]
// clear... // clear...
} else if(value === undefined){ } else if(value === undefined){
@ -393,6 +408,8 @@ module.pWikiPageActions = actions.Actions({
} else { } else {
d[name] = value d[name] = value
} }
// write the data...
// XXX is it good to write the whole thing??? // XXX is it good to write the whole thing???
this.data(d) this.data(d)
}], }],
@ -648,7 +665,6 @@ module.pWikiPageActions = actions.Actions({
} }
}], }],
__default_sort_methods__: ['path'],
__sort_methods__: { __sort_methods__: {
title: function(a, b){ title: function(a, b){
return a.page.title() < b.page.title() ? -1 return a.page.title() < b.page.title() ? -1
@ -718,8 +734,7 @@ module.pWikiPageActions = actions.Actions({
: [].slice.call(arguments) : [].slice.call(arguments)
res.__order_by = methods = methods.length == 0 ? res.__order_by = methods = methods.length == 0 ?
(this.__default_sort_methods__ (this.config['default-sort-methods'] || ['path'])
|| pWikiPage.__default_sort_methods__)
: methods : methods
res.update() res.update()
@ -754,11 +769,11 @@ module.pWikiPageActions = actions.Actions({
var reverse = false var reverse = false
var sort_methods = this.__sort_methods__ var sort_methods = this.__sort_methods__
|| pWikiPage.__sort_methods__ || pWikiBase.__sort_methods__
var methods = (this.__order_by var methods = (this.__order_by
|| this.__default_sort_methods__ || this.config['default-sort-methods']
|| pWikiPage.__default_sort_methods__) || ['path'])
.map(function(m){ .map(function(m){
var reversed = m[0] == '-' var reversed = m[0] == '-'
m = reversed ? m.slice(1) : m 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) var wiki = Object.create(pWikiData)
wiki.__data = Object.create(module._test_data) wiki.__data = Object.create(module._test_data)
var w = pWikiPageActions.clone() var w = pWikiPage.clone()
w.wiki = wiki w.wiki = wiki
return w return w
} }