From b5e0355886c77e49131c037541d09b8904af1a76 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Tue, 27 Sep 2016 04:44:15 +0300 Subject: [PATCH] split off the macros, still not ready but already working... Signed-off-by: Alex A. Naanou --- macro.js | 682 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ pwiki.js | 113 +++++++-- 2 files changed, 775 insertions(+), 20 deletions(-) create mode 100755 macro.js diff --git a/macro.js b/macro.js new file mode 100755 index 0000000..9653ccf --- /dev/null +++ b/macro.js @@ -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: + // + // + // HTML-like with body: + // + // ..text.. + // + // + // 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 +'' + 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 $('') + .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 }