MediaWiki:Gadget-libWikiDOM.js

/** * MediaWiki:Gadget-libWikiDOM.js * * WikiDOM will parse one page's wikitext * and creates an object with properties * representing the "nodes" or "tokens" like * Templates and their parameters, plain text, * Internal and External Links, Files * * It does *not* attempt to transform the wikitext * to HTML (? api action=parse or index.php action=render) * * It also provides easy ways for manipulating the wikitext * like replacing only areas that aren't comments or nowikis * * @rev 1 (2012-11-26) * @rev 2 (2013-06-13) Added DOM parser * @rev 3 (2014-04-16) Node.js integration * @author Rillke, 2012 * @author de:Benutzer:P.Copp, 2009 */ // List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/ // Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting] /*global mediaWiki:false, module:false, require:false, jQuery:false*/

// Set jsHint-options. You should not set forin or undef to false if your script does not validate. /*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true, smarttabs:true*/

(function {	"use strict";	var $;	if (typeof jQuery === 'undefined') {		$ = require('./jQuery.js');	} else {		$ = jQuery;	}	var mw;	if (typeof mediaWiki === 'undefined') {		mw = require('./mediaWiki.js');	} else {		mw = mediaWiki;	}

var wikiDOM;

wikiDOM = { /**		 * A powerful and easily extensible way to preserve certain areas during (regex-)?replaces * (c) 2012 Rainer Rillke, License: GPL, Documentation: GFDL *		 * @constructormethod *  @example *     var nwe1 = mw.libs.wikiDOM.nowikiEscaper(pageText1); *     var nwe2 = mw.libs.wikiDOM.nowikiEscaper(pageText2); *  @description *     You can pass an initial text to the object-function. *     The return value is an object. Perform all actions on the returned object! *		 * @methods * -getText *  @example var newPageText = nwe.getText *  @description -> retrieves the stored text *		 * -setText(pageText) *  @example nwe.setText(pageText) *  @description -> set a new text; overrides the old content *		 * -secureReplace(pattern, replace) *  @example nwe.secureReplace(pattern, replace) *  @description -> replace the pattern with replace; securely preserve nowikis *		 * -ordinaryReplace(pattern, replace) *  @example nwe.ordinaryReplace(, ) *  @description -> do an ordinary javaScript replace *		 * -replace(pattern, replace) *  @example nwe.replace(/(.)abc(.)/, '$2abc$1') *  @description -> replace the pattern with replace; allow extended use of substring  and $1..$9 *		 * -doCleanUp(pattern, replace) *  @example nwe.doCleanUp *  @description -> clean up the stored text *		 * -alsoPreserve(regexp) *  @example nwe.alsoPreserve('( (?:.|\n)*?<\/gallery>)') *  @description -> allows preserving other areas than just the predefined ones *		 **/		nowikiEscaper: function(inputText) { // Private // Data-variables var // The text is always kept up-to-date and contains the actual wikitext _text = inputText || '', // Contains the text where have been replaced by a placeholder placeholderText = '', // Contains the stripped nowikis from placeholderText nowikis = {}, // An array of objects [{ preserve: bool, text: textfragment }, ... ]				fragmentedText = [], // If the fragments are altered and this is not reflected in the placeholderText yet fragmentsAreDirty = false, // If the placeholderText is altered and this is not reflected in the fragmentedText yet placeholderTextDirty = false, // If the text was manipulated and this is not reflected in the "escaped" texts yet textDirty = false;

// Constants // The order is important here in cases where they match the same, the fist will be used var reToPreserve = [ /( (?:.|\n)*?<\/nowiki>)/i, /(]*>(?:.|\n)*?<\/source>)/i, /( (?:.|\n)*?<\/pre>)/i, /(]*>(?:.|\n)*?<\/syntaxhighlight>)/i, /(]*>(?:.|\n)*?<\/templatedata>)/i, /(<\!\-\-\s*categories\s*by\s*commonsense\s*\-\->)/i, /(<!\-\-(?:.|\n)*?\-\->)/ ];

var reToPreservePattern = /^\s*(\/\(\)\/)\s*$/;

// Functions var noMatchToInfinity = function(pos) { if (-1 === pos) return Infinity; return pos; };			var eachPosition = function(text, fn) { while (text) { /*jshint loopfunc:true*/ var arrPos = []; $.each(reToPreserve, function(i, regex) {						arrPos.push(noMatchToInfinity(text.search(regex)));					}); text = fn(text, arrPos); }			};			var nearestMatch = function(paramArray) { var validArgs = [], a = paramArray.length - 1;

for (a >= 0; a--) { if (-1 !== paramArray[a]) validArgs.push(paramArray[a]); }				// If validArgs.length is 0, Infinity (the biggest number possible) is returned return Math.min.apply(window, validArgs); };			var buildFragmentsFromText = function { fragmentedText = []; eachPosition(_text, function(text, arrPos) {					var nearestPos = nearestMatch(arrPos);					if (0 !== nearestPos) {						// Slice until this position						fragmentedText.push({ preserve: false, text: text.slice(0, nearestPos) });						return text.slice(nearestPos);					} else {						var newText;						$.each(arrPos, function(i, pos) { if (0 === pos) { newText = text.replace(reToPreserve[i], ''); fragmentedText.push({									preserve: true,									text: RegExp.$1								}); return false; }						});						return newText;					}				}); return fragmentedText; };			var buildPlaceholderText = function { placeholderText = ''; eachPosition(_text, function(text, arrPos) {					var nearestPos = nearestMatch(arrPos);					if (0 !== nearestPos) {						// Slice until this position						placeholderText += text.slice(0, nearestPos);						return text.slice(nearestPos);					} else {						var newText, rdm = '%v%f%c%' + Math.round(Math.random * 68719476736) + '%V%F%C%';						$.each(arrPos, function(i, pos) { if (0 === pos) { newText = text.replace(reToPreserve[i], ''); placeholderText += rdm; nowikis[rdm] = RegExp.$1; return false; }						});						return newText;					}				}); return placeholderText; };			var fragmentsToText = function { _text = ''; $.each(fragmentedText, function(i, fragmentObj) {					_text += fragmentObj.text;				}); return _text; };			var placeholderTextToText = function { _text = placeholderText; $.each(nowikis, function(id, nowikiContent) {					_text = _text.replace(id, nowikiContent);				}); return _text; };			var updateFragments = function { _text = placeholderTextToText; placeholderTextDirty = false; return buildFragmentsFromText; };			var updatePlaceholderText = function { _text = fragmentsToText; fragmentsAreDirty = false; return buildPlaceholderText; };			var updateEscapedText = function { buildFragmentsFromText; buildPlaceholderText; textDirty = false; };			if (_text) { textDirty = true; }

// Public interface return { setText: function(text) { _text = text; placeholderTextDirty = false; fragmentsAreDirty = false; textDirty = true; },				getText: function { if (placeholderTextDirty) placeholderTextToText; if (fragmentsAreDirty) fragmentsToText; return _text; },				secureReplace: function(pattern, replace) { if (textDirty) updateEscapedText; if (placeholderTextDirty) updateFragments; $.each(fragmentedText, function(i, fragmentObj) {						if (fragmentObj.preserve) return;						fragmentObj.text = fragmentObj.text.replace(pattern, replace);					}); fragmentsAreDirty = true; return this; },				ordinaryReplace: function(pattern, replace) { if (fragmentsAreDirty) updatePlaceholderText; if (placeholderTextDirty) updateFragments; _text = _text.replace(pattern, replace); textDirty = true; return this; },				replace: function(pattern, replace) { if (textDirty) updateEscapedText; if (fragmentsAreDirty) updatePlaceholderText; placeholderText = placeholderText.replace(pattern, replace); placeholderTextDirty = true; return this; },				alsoPreserve: function(regex) { if ('object' === typeof regex && regex.test) { reToPreserve.push(regex); return true; } else if ('string' === typeof regex) { var m = regex.match(reToPreservePattern); if (m && m[1]) { reToPreserve.push(new RegExp(m[1])); return true; }					}				},				doCleanUp: function { var i, l, rules = wikiDOM.cleanUpRules; for (i = 0, l = rules.length; i < l; ++i) { var rule = rules.headings[i], find = rule[0], regex = new RegExp('==\\s*[' + find.charAt(0).toUpperCase + find.charAt(0).toLowerCase + ']' + find.slice(1) + '\\s*==', '');

this.secureReplace(regex, rule[1]); }					for (i = 0, l = rules.wild.length; i < l; ++i) { this.secureReplace(rules.wild[i][0], rules.wild[i][1]); }					return this.getText; }			};		},		cleanUpRules: { // Rules can be found at File description page regular expressions headings: [ //['Summary', ''], //['Beschreibung', ''], ],			// Rules picked from https://commons.wikimedia.org/w/index.php?title=Commons:File_description_page_regular_expressions&oldid=80416934 wild: [ [/(\n)?==[ ]*(?:summary|sumario|descri(?:ption|pción|ção do arquivo)|achoimriú)(?:[ ]*\/[ ]*(?:summary|sumario|descri(?:ption|pción|ção do arquivo)|achoimriú))?[ ]*==/i, '$1== =='				],				[/\n==[ ]*(?:\[\[.*?\|)?(?:licen[cs](?:e|ing|ia)(?:[ ]*\/[ ]*licen[cs](?:e|ing|ia))?|\{\{int:license\}\})(?:\]\])?:?[ ]*==/i, '\n== ==' ],				[/\n==[ ]*(?:original upload (?:log|history)|file history|ursprüngliche bild-versionen)[ ]*==/i, '\n== =='], [/(\|[ ]*permission[ ]*=)\s*(?:-|see(?: licens(?:e|ing))?(?: below)?|yes|oui)\.?[ ]*(\||\}\}|\r|\n)/i, '$1$2'], [/(\|[ ]*other[_ ]versions\s*=)[ ]*(?:)?(?:-|no|none?(?: known)?)\.?(?:<\/i>)?[ ]*(\||\}\}|\r|\n)/i, '$1$2'], [/(\|[ ]*date[ ]*=\s*)(?:created|made|taken)?[ ]*([0-9]{4})(-| |\/|\.|)(0[1-9]|1[0-2])\3(1[3-9]|2[0-9]|3[01])(\||\}\}|\r|\n)/i, '$1$2-$4-$5$6'], [/(\|[ ]*date[ ]*=\s*)(?:created|made|taken)?[ ]*([0-9]{4})(-| |\/|\.|)(1[3-9]|2[0-9]|3[01])\3(0[1-9]|1[0-2])(\||\}\}|\r|\n)/i, '$1$2-$5-$4$6'], [/(\|[ ]*date[ ]*=\s*)(?:created|made|taken)?[ ]*(0[1-9]|1[0-2])(-| |\/|\.|)(1[3-9]|2[0-9]|3[01])\3([0-9]{4})(\||\}\}|\r|\n)/i, '$1$5-$2-$4$6'], [/(\|[ ]*date[ ]*=\s*)(?:created|made|taken)?[ ]*(1[3-9]|2[0-9]|3[01])(-| |\/|\.|)(0[1-9]|1[0-2])\3(2[0-9]{3}|1[89][0-9]{2})(\||\}\}|\r|\n)/i, '$1$5-$4-$2$6'], [/(\|[ ]*date[ ]*=\s*)(?:created|made|taken)?[ ]*\{\{date\|([0-9]{4})\|(0[1-9]|1[012])\|(0?[1-9]|1[0-9]|2[0-9]|3[01])\}\}(\||\}\}|\r|\n)/i, '$1$2-$3-$4$5'				],				[/__[ ]*NOTOC[ ]*__/, ''], [/?[ ]*\n?/, ''], [/[ ]*\[\[category[ ]*:[ ]*([^\]]*?)[ ]*(\|[^\]]*)?\]\][ ]*/, ''] ]		},		// mw.Title does not work for stuff like "information\n" normalizeTitle: function(t) { return $.trim(t.replace(/_/g, ' ')); },		/**		 * Normalize a template transclusion. *		 * @param {string} t * any transcluded template (e.g. 'Template:Abc\n' or 'abc '		 * @return {string} normalized result (e.g. 'Abc')		 */		normalizeTemplateTransclusion: function(t) {			var split = wikiDOM.normalizeTitle(t).split(':'),				templateNS = wikiDOM.getNamespaceNumber('template'),				maybeNS, shifted;			switch (split.length) {				case 0:					return '';				case 1:					return split[0];				default:					maybeNS = split[0].toLowerCase.replace(/ /g, '_');					$.each(mw.config.get('wgNamespaceIds'), function(key, n) { if (maybeNS === key && n === templateNS) { split.shift; shifted = true; return false; }					});

// Assume that the first part is a namespace if (!shifted) wikiDOM.normalizeTitle(split[1]); return wikiDOM.normalizeTitle(split.join(':')); }		},		/**		 * Normalize a link (mostly useful for files and categories) *		 * @param {string} l * Any link (without the square-brackets) (e.g. 'Image:a.png') * @return {string} normalized result (e.g. 'File:A.png') */		normalizeLink: function(l) { var split = wikiDOM.normalizeTitle(l).split(':'), ns, escape = ''; switch (split.length) { case 0: return ''; case 1: return split[0]; default: if (!split[0]) { escape = ':'; split.shift; }					try { split[0] = wikiDOM.getLocalizedNamespace(split[0]); ns = split.shift; split = [ns, wikiDOM.normalizeTitle(split.join(':'))]; } catch(ex) {} return escape + split.join(':'); }		},		/**		 * Namespace number from namespace-string. *		 * @param {string} ns Namespace * @return {number} Namespace number */		getNamespaceNumber: function(ns) { return mw.config.get('wgNamespaceIds')[ns.toLowerCase.replace(/ /g, '_')]; },

/**		 * Namespace in content language from any * namespace alias. *		 * @param {string} ns Namespace * @return {string} Namespace in content language */		getLocalizedNamespace: function(ns) { return mw.config.get('wgFormattedNamespaces')[ wikiDOM.getNamespaceNumber(ns) ]; },		/**		 * @description * mw.libs.wikiDOM.parser *		 *		 * @methods *  -text2Obj(wikiMarkup, forInclusion) *  @param {string} wikiMarkup *  @param {boolean} forInclusion [optional] *  @return {Node} *		 *  -obj2Text(Node) *  @param {Node} A wikiDOM node that will be converted to a plain String. *  @return {string} wikiMarkup *		 *		 * @example // parse into a DOMObject var o = mw.libs.wikiDOM.parser.text2Obj( $('#wpTextbox1').val ); // manipulate the object o.parts[0][2].parts[1][0] = "other text"; o.parts[0][2].after("text"); // convert the DOMObject back to a string of raw wikitext $('#wpTextbox1').val( mw.libs.wikiDOM.parser.obj2Text(o) ); *		 *		 The root node object has the following structure: {				nodesByType: { nodetype: [] },				type: 'root', parts: [ [node, string, string, node, ...] ]			}		 Only the root node has the list "nodesByType" This is how a node object looks like: {				len: number, lineStart: boolean, linktype: 'category'|'file', offset: number, parent: Node, parts: Array of nodes and Strings , type: 'root'|'link'|'template'|'tplarg'|'h'|'comment'|'ignore'|'ext', extname: 'name', //only for ext nodes index, level : int, //only for heading nodes // Manipulation functions: after: function(String or Node){}, before: function(String or Node){}, append: function(String or Node){}, prepend: function(String or Node){}, insert: function(String or Node, Offset){} }			// where link is everything enclosed by square brackets,, , h is a heading, // c a, ext an extension tag with content * Note that after using one of the manipulation functions, * the values len and offset may be wrong *		 *		 * Files and Categories are reported as links but linktype specifies of what * subtype they are. * 		 * The content of Extensiontags (ext) is not analyzed or parsed. *		 *		 * preprocessor.js		 * Wikitext preprocessor, based on MediaWiki's parser (Preprocessor_DOM.php r55795) * http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/parser/Preprocessor_DOM.php *		 *		 * @source * Derivative work of https://de.wikipedia.org/w/index.php?curid=4150203&oldid=68296956 * de:Benutzer:P.Copp/scripts/preprocessor.js * which is in turn a port of the MediaWiki preprocessor to JavaScript * Copyright by Benutzer:P.Copp at German Wikipedia and contributors to MediaWiki * GPL, GFDL */		parser: (function {

var extensiontags = ['categorytree', 'charinsert', 'hiero', 'imagemap', 'inputbox', 'languages', 'poem', 'ref', 'references', 'source', 'syntaxhighlight', 'timeline', 'templatedata' ],				defaulttags = ['nowiki', 'gallery', 'math', 'pre', 'noinclude', 'includeonly', 'onlyinclude'], nsIds = mw.config.get('wgNamespaceIds'), generateRE = function(nsId) { var nsTokens = []; $.each(nsIds, function(k, id) {						if (nsId === id) {							k = '[' + mw.RegExp.escape(k.charAt(0).toUpperCase) + mw.RegExp.escape(k.charAt(0).toLowerCase) + ']' + mw.RegExp.escape(k.slice(1)).replace(/_/g, '[ _]');							nsTokens.push(k);						}					}); return new RegExp('^(?:' + nsTokens.join('|') + ')\\:'); },				linktypes = { category: generateRE(nsIds.category), file: generateRE(nsIds.file) };

/**************************************************************************************************			 * text2Obj *			 * Turns a wikitext string into a document tree * The returned data structure is a bit more compact than a real XML DOM, so * some memory is saved, when the extra stuff is not needed. *			 * The returned object has the following structure: * domnode = { *    type  : ('root'|'link'|'template'|'tplarg'|'h'|'comment'|'ignore'|'ext'), *    offset: int, *    len   : int, *    parts : [  [('text'|node)*],  ... ],			 *    index, level : int, //only for heading nodes *    extname: 'name', //only for ext nodes * }			 *			 * Dependencies: extensiontags, defaulttags */			var text2Obj = function(text, forInclusion) { if (text === false) return text;

//DOM Node var Node = function(type, offset, content, count) { this.type = type; this.offset = offset; this.parts = [ []					];					this.linktype = '';

// Some node types are not properly specified at this time if (type !== '[' && type !== '{') { nodesByType[type] = nodesByType[type] || []; nodesByType[type].push(this); }

//cur and count are only for internal processing. //They will be cleaned up later by finish this.cur = this.parts[0]; if (content) add(this, content); if (count) this.count = count; };

// Note that these methods invalidate the offset etc.				Node.prototype = $.extend(Node.prototype, {					append: function(x) {						var t = this;						if ('root' === t.type) {							t.parts[0].push(x);						} else {							t.parts.push([x]);						}						return t;					},					prepend: function(x) {						var t = this;						if ('root' === t.type) {							t.parts[0].unshift(x);						} else {							t.parts.unshift([x]);						}						return t;					},					after: function(x) {						return this.insert(x, 1);					},					before: function(x) {						return this.insert(x, 0);					},					insert: function(x, offset) {						var t = this,							p = t.parent;

// Error: Cannot insert before or after root node if (!p) return false;

var pplen = p.parts.length, i, pi, pilen;

for (i = 0; i < pplen; i++) { pi = p.parts[i]; // Don't loop over Strings! if (!$.isArray(pi)) return; pilen = pi.length; for (var idx = 0; idx < pilen; idx++) { var el = pi[idx]; if (el === t) { pi.splice(idx + offset, 0, x); return this; }							}						}						return false; }				});

var lastindex = 0, stack = [], nodesByType = {}, top = new Node('root', 0), headings = 0, skipnewline = false, tag = null, enableonlyinclude = false, search = false, match;

//Line 145-156 if (forInclusion && text.indexOf(' ') > -1 && text.indexOf(' ') > -1) { enableonlyinclude = true; tag = new Node('ignore', 0); search = / |^$/; }				var ignoredtag = forInclusion ? /includeonly/i : /noinclude|onlyinclude/i; var ignoredelement = forInclusion ? 'noinclude' : 'includeonly';

//Construct our main regex var tags = '(' + defaulttags.concat(extensiontags).join('|') + ')'; var specials = '\\{\\{+|\\[\\[+|\\}\\}+|\\]\\]+|\\||(\n)(=*)|(^=+)'; var regex = new RegExp(specials + '|<' + tags + '(?:\\s[^>]*)?\\/?>|||$', 'ig');

while (!!(match = regex.exec(text))) { var s = match[0];

//If we're in searching mode, skip all tokens until we find a matching one if (search) { if (s.match(search)) { search = false; if (tag.type !== 'comment') { add(tag, text.substring(lastindex, match.index)); lastindex = match.index + s.length; if (tag.type !== 'ignore') tag.parts.push(tag.cur = []); add(tag, s); processToken('tag', finish(tag, match.index + s.length)); }						}						continue; }

if (s === '|^$/;						//If we put a trailing newline in the comment, make sure we don't double output it						if (text.charAt(span[1] - 1) === '\n') skipnewline = true;						continue;					}

//Process all text between the last and the current token if (match.index > lastindex) processToken('text', text.substring(lastindex, match.index)); lastindex = match.index + s.length; if (!s) break; //End of text

if (match[1] || match[3]) { //Line start/end if (skipnewline || match[3]) skipnewline = false; else { processToken('lineend', '', match.index); processToken('text', '\n'); }						//processToken( 'linestart' ); if (match[2] || match[3]) processToken('=', match[2] || match[3], match.index + (match[1] ? 1 : 0));						continue; }

if (match[4]) { //Open  found if (match[4].match(ignoredtag)) { processToken('tag', finish(new Node('ignore', match.index, s), lastindex)); continue; }						var lc = match[4].toLowerCase; if (lc === 'onlyinclude') { //This can only happen, if we're in template mode (forInclusion=true) and //the token we found is sth. like ''(i.e. unusual case or whitespace) //Output it literally then, to match MediaWiki's behavior processToken('text', s); } else { if (lc === ignoredelement) tag = new Node('ignore', match.index, s); else { tag = new Node('ext', match.index, s); tag.extname = lc; }							if (s.charAt(s.length - 2) === '/') { //Immediately closed tag (e.g. ) processToken('tag', finish(tag, match.index + s.length)); } else { //Search for the matching closing tag search = new RegExp('<\\/' + lc + '\\b|^$', 'i'); //For ext nodes, we split the opening tag, content and closing tag into //separate parts. This is to simplify further processing since we already have //the information after all if (lc !== ignoredelement) tag.parts.push(tag.cur = []); }						}						continue;

} else if (match[5]) { //Close found if (match[5].match(ignoredtag)) { processToken('ignore',								finish(new Node('ignore', match.index, s), lastindex)); } else if (enableonlyinclude && s === ' ') { //For onlyinclude, the closing tag is the start of the ignored part tag = new Node('ignore', match.index, s); search = / |^$/; } else { //We don't have a matching opening tag, so output the closing literally processToken('text', s); }						continue; } else if (s === '-->') { //Comment endings without openings are output normally processToken('text', s); continue; }					//Special token found: '|', {+, [+, ]+, }+ var ch = s.charAt(0); processToken(ch, s, match.index); }				//End of input. Put an extra line end to make sure all headings get closed properly processToken('lineend', text.length); processToken('end', text.length); postProcess;

return stack[0];

function postProcess { var lts = linktypes; nodesByType.link = nodesByType.link || []; $.each(nodesByType.link, function(i, n) {						for (var lt in lts) {							if (lts.hasOwnProperty(lt)) {								var re = lts[lt];								if (n.parts[0] && re.test(n.parts[0])) {									n.linktype = lt;									nodesByType[lt] = nodesByType[lt] || [];									nodesByType[lt].push(n);									break;								}							}						}					}); stack[0].nodesByType = nodesByType; }

//Handle some token and put it in the stack

function processToken(type, token, offset) { var next, len; switch (type) { case 'text': case 'ignore': case 'tag': return add(top, token); case 'lineend': //Check if we can close a heading if (top.type === 'h') { next = stack.pop; if (top.closing) { //Some extra info for headings top.index = ++headings; top.level = Math.min(top.count, top.closing, 6); add(next, finish(top, offset)); } else { //No correct closing, break the heading and continue addBrokenNode(next, top); }								top = next; }							return; case '=': //Check if we can open a heading len = token.length; //Line 352-355: Single '=' within a template part isn't treated as heading if (len === 1 && top.type === '{' && top.parts.length > 1 && top.cur.splitindex === undefined) { add(top, token); } else { stack.push(top); top = new Node('h', offset, token, len); //Line 447-455: More than two '=' means we already have a correct closing top.closing = Math.floor((len - 1) / 2); }							return; case '|': //For brace nodes, start a new part if (top.type === '[' || top.type === '{') top.parts.push(top.cur = []); else add(top, token); return; case '{': case '[': stack.push(top); top = new Node(type, offset, '', token.length); return; case '}': case ']': //Closing brace found, try to close as many nodes as possible var open = type === '}' ? '{' : '[';							len = token.length; while (open === top.type && len >= 2) { while (len >= 2 && top.count >= 2) { //Find the longest possible match var mc = Math.min(len, top.count, open === '{' ? 3 : 2); top.count -= mc; len -= mc; //Record which type of node we found if (open === '{') top.type = mc === 2 ? 'template' : 'tplarg'; else top.type = 'link';

nodesByType[top.type] = nodesByType[top.type] || []; nodesByType[top.type].push(top);

if (top.count >= 2) { //if we're still open, create a new parent and embed the node there var child = top; top = new Node(open, child.offset, child, child.count); //Correct the child offset by the number of remaining open braces child.offset += top.count; finish(child, offset + token.length - len); }								}								if (top.count < 2) { //Close the current node next = stack.pop; //There might be one remaining brace open, add it to the parent first if (top.count === 1) add(next, open); top.offset += top.count; add(next, finish(top, offset + token.length - len)); top = next; }							}							//Remaining closing braces are added as plain text if (len) add(top, (new Array(len + 1)).join(type)); return; case 'end': //We've reached the end, expand any remaining open pieces stack.push(top); for (var i = 1; i < stack.length; i++) addBrokenNode(stack[0], stack[i]); finish(stack[0], offset); }				}

//Helper function to calculate the start and end position of a comment //We need this, because comments sometimes include the preceding and trailing whitespace //See lines 275-313

function getCommentSpan(start) { var endpos = text.indexOf('-->', start + 4); if (endpos === -1) return [start, text.length]; for (var lead = start - 1; text.charAt(lead) === ' '; lead--); if (text.charAt(lead) !== '\n') return [start, endpos + 3]; for (var trail = endpos + 3; text.charAt(trail) === ' '; trail++); if (text.charAt(trail) !== '\n') return [start, endpos + 3]; return [lead + 1, trail + 1]; }

//Append text or a child to a node

function add(node, el) { if (!el) return; var newstr = typeof el === 'string'; var oldstr = typeof node.cur[node.cur.length - 1] === 'string';

if (!newstr) el.parent = node;

if (newstr && oldstr) node.cur[node.cur.length - 1] += el; else node.cur.push(el);

//For template nodes, record if and where an equal sign was found if (newstr && node.type === '{' && node.cur.splitindex === undefined && el.indexOf('=') > -1) node.cur.splitindex = node.cur.length - 1;

//For heading nodes, record if we have a correct closing //A heading must end in one or more equal signs, followed only by					//whitespace or comments if (node.type === 'h') { if (newstr) { var match = el.match(/(=+)[ \t]*$/); if (match) node.closing = match[1].length; else if (!el.match(/^[ \t]*$/)) node.closing = false; } else if (el.type !== 'comment') node.closing = false; }				}

//Break and append a child to a node

function addBrokenNode(node, el) { //First add the opening braces if (el.type !== 'h') add(node, (new Array(el.count + 1)).join(el.type)); //Then the parts, separated by '|' for (var i = 0; i < el.parts.length; i++) { if (i > 0) add(node, '|'); for (var j = 0; j < el.parts[i].length; j++) add(node, el.parts[i][j]); }				}

//Clean up the extra stuff we put into the node for easier processing

function finish(node, endOffset) { node.len = endOffset - node.offset; node.lineStart = text.charAt(node.offset - 1) === '\n'; delete node.cur; delete node.count; delete node.closing; return node; }			};

/**************************************************************************************************			 * PPFrame : Basic expansion frame, transforms a document tree back to the original wikitext */

function PPFrame { this.self = PPFrame; }			PPFrame.prototype = $.extend(PPFrame.prototype, {				onEvent: $.noop, // function(evt, node, result, info) {}

expand: function(obj) { var result; if (typeof obj === 'string') { result = this.expandString(obj); this.onEvent('text', obj, result); return result; }					var type = obj.type.charAt(0).toUpperCase + obj.type.substring(1); var func = this['expand' + type]; if (!func) throw new Error('Unknown node type: ' + obj.type); this.onEvent('enter' + type, obj); result = func.call(this, obj); this.onEvent('leave' + type, obj, result); return result; },

expandDeleted: function { return ''; },				expandString: function(s) { return s;				}, expandRoot: function(obj) { return this.expandPart(obj.parts[0]); },				expandLink: function(obj) { return this.expand() + this.expand(); },				expandTemplate: function(obj) { return this.expand(''); },				expandTplarg: function(obj) { return this.expand(''); },				expandH: function(obj) { return this.expandPart(obj.parts[0]); },				expandComment: function(obj) { return this.expand(obj.parts[0][0]); },				expandIgnore: function(obj) { return this.expand(obj.parts[0][0]); },				expandExt: function(obj) { return this.expandParts(obj.parts); },

expandPart: function(part) { var result = ''; for (var i = 0; i < part.length; i++) result += this.expand(part[i]); return result; },				expandParts: function(parts, joiner) { var result = ''; for (var i = 0; i < parts.length; i++) { if (joiner && i > 0) result += this.expand(joiner); result += this.expandPart(parts[i]); }					return result; },

splitPart: function(part) { var i = part.splitindex; if (i === undefined) return false; var pos = part[i].indexOf('='); var name = part.slice(0, i); name.push(part[i].substring(0, pos)); var value = [part[i].substring(pos + 1)].concat(part.slice(i + 1)); return [name, value]; },

extractParams: function(obj) { var params = { //numbered and named arguments must be stored separately numbered: {}, named: {}, obj: obj };					var num = 1; for (var i = 1; i < obj.parts.length; i++) { var split = this.splitPart(obj.parts[i]); if (split) { var name = this.expandArgName(obj, split[0], i); params.named[name] = { value: split[1], part: i							}; } else params.numbered[num++] = { part: i						}; }					return params; },				getParam: function(params, name) { for (var i = 0; i < 2; i++) { var type = i ? 'named' : 'numbered'; var param = params[type][name]; if (!param) continue; if (typeof param.value === 'string') return param.value; //cached //Param exists, but not yet expanded. Expand it and put the result in the cache param.value = i ? this.expandArgValue(params.obj, param.value, param.part) : this.expandArg(params.obj, param.part); return param.value; }					return false; },

expandArgName: function(obj, part, num) { this.onEvent('enterArgName', obj, null, [part, num]); var result = this.expandPart(part).trim; this.onEvent('leaveArgName', obj, result, [part, num]); return result; },				expandArgValue: function(obj, part, num) { this.onEvent('enterArgValue', obj, null, [part, num]); var result = this.expandPart(part).trim; this.onEvent('leaveArgValue', obj, result, [part, num]); return result; },				expandArg: function(obj, num) { if (obj.parts[num] === undefined) return ''; this.onEvent('enterArg', obj, null, num); var result = this.expandPart(obj.parts[num]); this.onEvent('leaveArg', obj, result, num); return result; }			});

var ppFrame = new PPFrame;

return { text2Obj: text2Obj, obj2Text: function(o) { return ppFrame.expand(o); }			};		})	};

if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = wikiDOM; }	if (typeof mw !== 'undefined') { // Expose globally mw.libs.wikiDOM = wikiDOM; } });