/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var setWikiWords = function(text, show_brackets, skip){ skip = skip || [] skip = skip instanceof Array ? skip : [skip] return text // set new... .replace( macro.__wiki_link__, function(l){ // check if WikiWord is escaped... if(l[0] == '\\'){ return l.slice(1) } var path = l[0] == '[' ? l.slice(1, -1) : l var i = [].slice.call(arguments).slice(-2)[0] // XXX HACK check if we are inside a tag... var rest = text.slice(i+1) if(rest.indexOf('>') < rest.indexOf('<')){ return l } return skip.indexOf(l) < 0 ? ('' + (!!show_brackets ? path : l) +'') : l })} /*********************************************************************/ 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', ], // XXX should this be here??? __wiki_link__: RegExp('('+[ //'\\\\?(\\/|\\./|\\.\\./|>>|[A-Z][_a-z0-9]+[A-Z/])[_a-zA-Z0-9/]*', '\\\\?\\/?(\\./|\\.\\./|>>|[A-Z][_a-z0-9]+[A-Z/])[_a-zA-Z0-9/]*', '\\\\?\\[[^\\]]+\\]', ].join('|') +')', 'g'), // 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 // // NOTE: we do not set the title tag here because this will be // done for every included page... 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()) } 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\/)+/ // XXX slow when lots of pages need to be included... 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... // NOTE: we need to do this like this to avoid // unparsing special characters... 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) // XXX DEBUG... var t = Date.now() console.log('>>>', context.path()) // 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') })) // XXX DEBUG... //console.log('<<<', context.path(),'TIME:', Date.now() - t) // 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 })