Welcome to the DFO World Wiki. With many major updates since the release of DFO, many items are missing. Visit Item Database Project to learn more.
Please remember to click "show preview" before saving the page.
Thanks for the updated logo snafuPop!

MediaWiki:Gadget-libWikiDOM.js

From DFO World Wiki
Jump to: navigation, search

Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/**
 * [[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('<!-- Comment to remove -->', '')
		 *   @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('(<gallery>(?:.|\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 <nowikis> 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 = [
					/(<nowiki>(?:.|\n)*?<\/nowiki>)/i,
					/(<source [^\>]*>(?:.|\n)*?<\/source>)/i,
					/(<pre>(?:.|\n)*?<\/pre>)/i,
					/(<syntaxhighlight [^\>]*>(?:.|\n)*?<\/syntaxhighlight>)/i,
					/(<templatedata[^\>]*>(?:.|\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 [[Commons:File description page regular expressions]]
			headings: [
				//['Summary', '{{int:filedesc}}'],
				//['Beschreibung', '{{int:filedesc}}'],
			],
			// 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== {{int:filedesc}} =='
				],
				[/\n==[ ]*(?:\[\[.*?\|)?(?:licen[cs](?:e|ing|ia)(?:[ ]*\/[ ]*licen[cs](?:e|ing|ia))?|\{\{int:license\}\})(?:\]\])?:?[ ]*==/i,
						'\n== {{int:license-header}} =='
				],
				[/\n==[ ]*(?:original upload (?:log|history)|file history|ursprüngliche bild-versionen)[ ]*==/i, '\n== {{original upload log}} =='],
				[/(\|[ ]*permission[ ]*=)\s*(?:-|see(?: licens(?:e|ing))?(?: below)?|yes|oui)\.?[ ]*(\||\}\}|\r|\n)/i, '$1$2'],
				[/(\|[ ]*other[_ ]versions\s*=)[ ]*(?:<i>)?(?:-|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[ ]*__/, ''],
				[/(<!--)?[ ]*\{\{ImageUpload\|(?:full|basic)\}\}[ ]*(-->)?[ ]*\n?/, ''],
				[/[ ]*\[\[category[ ]*:[ ]*([^\]]*?)[ ]*(\|[^\]]*)?\]\][ ]*/, '[[Category:$1$2]]']
			]
		},
		
		// mw.Title does not work for stuff like "information\n"
		normalizeTitle: function(t) {
			return $.ucFirst($.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]], {{template}}, {{{tplarg}}}, h is a heading,
			// c a <!-- comment -->, 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 = '[' + $.escapeRE(k.charAt(0).toUpperCase()) + $.escapeRE(k.charAt(0).toLowerCase()) + ']' + $.escapeRE(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('<onlyinclude>') > -1 && text.indexOf('</onlyinclude>') > -1) {
					enableonlyinclude = true;
					tag = new Node('ignore', 0);
					search = /<onlyinclude>|^$/;
				}
				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[^>]*)?\\/?>|<\\/' + 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 === '<!--') { //Comment found
						var span = getCommentSpan(match.index);
						processToken('text', text.substring(lastindex, span[0]));
						lastindex = span[1];
						tag = new Node('comment', span[0], text.substring(span[0], span[1]));
						processToken('tag', finish(tag, span[1]));
						search = /-->|^$/;
						//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 <tag /?> 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 '<ONLYINCLUDE >'(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. <nowiki />)
								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 </tag> found
						if (match[5].match(ignoredtag)) {
							processToken('ignore',
								finish(new Node('ignore', match.index, s), lastindex));
						} else if (enableonlyinclude && s === '</onlyinclude>') {
							//For onlyinclude, the closing tag is the start of the ignored part
							tag = new Node('ignore', match.index, s);
							search = /<onlyinclude>|^$/;
						} 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.expandParts(obj.parts, '|') + this.expand(']]');
				},
				expandTemplate: function(obj) {
					return this.expand('{{') + this.expandParts(obj.parts, '|') + this.expand('}}');
				},
				expandTplarg: function(obj) {
					return this.expand('{{{') + this.expandParts(obj.parts, '|') + 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;
	} else {
		// Expose globally
		mw.libs.wikiDOM = wikiDOM;
	}
}());