Module:User:Theknightwho/template parser

This is a private module sandbox of Theknightwho, for their own experimentation. Items in this module may be added and removed at Theknightwho's discretion; do not rely on this module's stability.


setmetatable(package.loaded, {
	__mode = "v"
})

local require = require
local assert = assert
local concat = table.concat
local floor = math.floor
local format = string.format
local gmatch = string.gmatch
local gsub = string.gsub
local insert = table.insert
local ipairs = ipairs
local lower = string.lower
local match = string.match
local min = math.min
local new_title = mw.title.new
local nowiki = require("Module:string utilities").nowiki
local pcall = pcall
local rawset = rawset
local remove = table.remove
local rep = string.rep
local setmetatable = setmetatable
local sub = string.sub
local tonumber = tonumber
local type = type
local ulen = mw.ustring.len
local ulower = string.ulower
local upper = string.upper
local uri_encode = mw.uri.encode

mw.loadData = require

local m_parser = require("Module:parser")

local TAGS = {
	categorytree = true,
	ce = true,
	charinsert = true,
	chem = true,
	dynamicpagelist = true,
	gallery = true,
	graph = true,
	hiero = true,
	imagemap = true,
	indicator = true,
	inputbox = true,
	langconvert = true,
	mapframe = true,
	maplink = true,
	math = true,
	nowiki = true,
	poem = true,
	pre = true,
	ref = true,
	references = true,
	score = true,
	section = true,
	source = true,
	syntaxhighlight = true,
	talkpage = true,
	templatedata = true,
	templatestyles = true,
	thread = true,
	timeline = true
}

local export = {}

------------------------------------------------------------------------------------
--
-- Helper functions
--
------------------------------------------------------------------------------------

local function is_space(this)
	return this == " " or
		this == "\t" or
		this == "\n" or
		this == "\v" or
		this == "\f" or
		this == "\r"
end

-- Standard PHP character escape.
local function php_escaped(text)
	return (gsub(text, "[\"&'<>]", {
		["\""] = "&quot;", ["&"] = "&amp;", ["'"] = "&#039;",
		["<"] = "&lt;", [">"] = "&gt;",
	}))
end

local function tonumber_loose(text)
	if type(text) == "string" then
		local text_lower = lower(text)
		return text_lower ~= "inf" and
			text_lower ~= "-inf" and
			text_lower ~= "nan" and
			text_lower ~= "-nan" and
			tonumber(text) or text
	end
	return text
end

local function tonumber_strict(text)
	if type(text) == "string" then
		local num_text = match(text, "^[+%-]?%d+%.?%d*")
		text = tonumber(num_text) or text
	end
	return text
end

local function trim(str)
	if type(str) ~= "string" then
		return str
	end
	local n
	for i = 1, #str do
		if not is_space(sub(str, i, i)) then
			n = i
			break
		end
	end
	if not n then
		return ""
	end
	for i = #str, n, -1 do
		if not is_space(sub(str, i, i)) then
			return sub(str, n, i)
		end
	end
end

------------------------------------------------------------------------------------
--
-- Frame
--
------------------------------------------------------------------------------------

local Frame = mw.getCurrentFrame()
local actual_parent = Frame:getParent()

do
	local function eq(a, b)
		return rawequal(a, b) or rawequal(b, Frame)
	end
	
	setmetatable(Frame, {__eq = eq})
	
	local function newCallbackParserValue(callback)
		local value, cache = {}
	
		function value:expand()
			if not cache then
				cache = callback()
			end
			return cache
		end
	
		return value
	end
	
	function Frame:getArgument(opt)
		local name = type(opt) == "table" and opt.name or opt
		return newCallbackParserValue(function()
			return self.args[name]
		end)
	end
	
	function Frame:getParent()
		return nil
	end
	
	Frame.really_preprocess = Frame.preprocess
	
	function Frame:preprocess(opt)
		return export.parse(opt, self:getTitle()):expand()
	end
	
	function Frame:newParserValue(opt)
		local text = type(opt) == "table" and opt.text or opt
		return newCallbackParserValue(function()
			return self:preprocess(text)
		end)
	end
	
	function Frame:newTemplateParserValue(opt)
		assert(
			type(opt) == "table",
			"frame:newTemplateParserValue: the first parameter must be a table"
		)
		assert(
			opt.title,
			"frame:newTemplateParserValue: a title is required"
		)
		return newCallbackParserValue(function()
			return self:expandTemplate(opt)
		end)
	end
	
	function Frame:argumentPairs()
		return pairs(self.args)
	end
	
	function Frame:newChild(opt)
		assert(
			type(opt) == "table",
			"frame:newChild: the first parameter must be a table"
		)
		local title = opt.title and tostring(opt.title) or self:getTitle()
		assert(
			not opt.args or type(opt.args) == "table",
			"frame:newChild: args must be a table"
		)
		
		local child = setmetatable({
			args = opt.args or {}
		}, {
			__index = Frame,
			__eq = eq
		})
		
		function child:getTitle()
			return title
		end
		
		if opt.parent ~= false then
			function child.getParent()
				return self
			end
		end
		
		return child
	end
end

local parent_frame
local child_frame = Frame:newChild{
	title = Frame:getTitle(),
	args = Frame.args,
	parent = false
}

function mw.getCurrentFrame()
	return child_frame
end

------------------------------------------------------------------------------------
--
-- Nodes
--
------------------------------------------------------------------------------------

local Node = m_parser.Node

function Node:expand_child(key, args, ...)
	if type(self[key]) == "table" and not self[key].cached then
		self[key] = self[key]:expand(false, ...)
		child_type = type(self[key])
		pcall(rawset, self[key], "cached", true)
	end
	return type(self[key]) == "table" and self[key]:expand(args, ...) or self[key]
end

local Wikitext = m_parser.Wikitext

function Wikitext:expand(args)
	local output, arg = {}
	for i in ipairs(self) do
		arg = self:expand_child(i, args)
		if type(arg) == "table" then
			output = false
		elseif output then
			insert(output, arg)
		end
	end
	return output and concat(output) or self
end

local Tag = Node:new("tag")

function Tag:__tostring()
	local open_tag = {"<", self.name}
	if self.ignored then
		return ""
	elseif self.attributes then
		for attr, value in pairs(self.attributes) do
			insert(open_tag, " " .. attr .. "=\"" .. value .. "\"")
		end
	end
	if self.self_closing then
		insert(open_tag, "/>")
		return concat(open_tag)
	end
	insert(open_tag, ">")
	return concat(open_tag) .. concat(self) .. "</" .. self.name .. ">"
end

function Tag:expand(args) -- FIXME
	if self.ignored then
		return ""
	end
	return self:__tostring()
end

local Argument = Node:new("argument")

function Argument:__tostring()
	if self[2] then
		local output, i = {"{{{", tostring(self[1])}, 2
		while self[i] do
			insert(output, "|")
			insert(output, tostring(self[i]))
			i = i + 1
		end
		insert(output, "}}}")
		return concat(output)
	elseif self[1] then
		return "{{{" .. tostring(self[1]) .. "}}}"
	else
		return "argument"
	end
end

function Argument:next()
	self.i = self.i + 1
	if self.i <= 2 then
		return self[self.i]
	end
end

function Argument:expand(args)
	if args == false then
		return self
	end
	local arg1 = self:expand_child(1, args)
	if type(arg1) == "string" then
		arg1 = tonumber_loose(arg1)
	end
	if args and args[arg1] then
		return args[arg1]
	end
	local arg2 = self:expand_child(2, args)
	return arg2 or "{{{" .. arg1 .. "}}}"
end

local Parameter = Node:new("parameter")

function Parameter:__tostring()
	if self.key then
		return tostring(self.key) .. "=" .. Node.__tostring(self)
	end
	return Node.__tostring(self)
end

function Parameter:expand(args, array)
	if array and self.key then
		local a, b = self:expand_child("key", args), Wikitext.expand(self, args)
		if args == false and (type(a) == "table" or type(b) == "table") then
			return Wikitext:new{a, "=", b}
		end
		return a .. "=" .. b
	end
	return Wikitext.expand(self, args)
end

local Template = Node:new("template")

function Template:__tostring()
	if self[2] then
		local output, n = {"{{", tostring(self[1])}, 2
		if self.colon then
			insert(output, ":")
			insert(output, tostring(self[3]))
			n = 3
		end
		for i = n, #self do
			insert(output, "|")
			insert(output, tostring(self[i]))
		end
		insert(output, "}}")
		return concat(output)
	elseif self[1] then
		return "{{" .. tostring(self[1]) .. "}}"
	else
		return "template"
	end
end

function Template:get_params(args)
	local params, implicit, key, value, n = {}, 0
	for i = 2, #self do
		if self[i].key then
			key = self[i]:expand_child("key", args)
			if type(key) == "string" then
				key = tonumber_loose(key)
			end
			-- We need to retain the parameter object if there's an explicit key.
			value = self[i]:expand(args)
		else
			implicit = implicit + 1
			key = implicit
			value = self:expand_child(i, args)
		end
		if args == false and (type(key) == "table" or type(value) == "table") then
			params = false
		elseif params then
			params[key] = value
		end
	end
	return params
end

function Template:get_array_params(args, max, max_eval)
	max = max + 1 or #self
	max_eval = max_eval and (max_eval + 1) or max
	local params, value = {}
	for i = 2, max do
		value = i <= max_eval and self:expand_child(i, args, true) or self[i]
		if args == false and type(value) == "table" then
			params = false
		elseif params then
			params[i - 1] = value
		end
	end
	return params
end

function Template:parser_function_error(mw_page, ...)
	local msg = new_title("MediaWiki:" .. mw_page):getContent()
	for i, v in ipairs(arg) do
		msg = gsub(msg, "$" .. i, v)
	end
	return export.parse("<strong class=\"error\">" .. php_escaped(msg) .. "</strong>", true):expand()
end

do
	local getCurrentTitle
	do
		local current_title
		function getCurrentTitle()
			current_title = current_title or mw.title.getCurrentTitle()
			return current_title
		end
	end
	
	local getContentLanguage
	do
		local content_lang
		function getContentLanguage()
			content_lang = content_lang or mw.getContentLanguage()
			return content_lang
		end
	end
	
	local getPageLanguage
	do
		local page_lang
		function getPageLanguage()
			page_lang = page_lang or mw.language.new(Frame:really_preprocess("{{PAGELANGUAGE}}"))
			return page_lang
		end
	end
	
	local case_insensitive = {}
	
	function case_insensitive:__index(k)
		local k_upper = upper(k)
		if type(k) == "string" and case_insensitive[k_upper] then
			return rawget(self, k_upper)
		end
	end
	
	for _, k in ipairs{"#BABEL", "#CATEGORYTREE", "#DATEFORMAT", "#EXPR", "#FORMATDATE", "#IF", "#IFEQ", "#IFERROR", "#IFEXIST", "#IFEXPR", "#INVOKE", "#LANGUAGE", "#LQTPAGELIMIT", "#LST", "#LSTH", "#LSTX", "#PROPERTY", "#REL2ABS", "#SECTION", "#SECTION-H", "#SECTION-X", "#SPECIAL", "#SPECIALE", "#STATEMENTS", "#SWITCH", "#TAG", "#TARGET", "#TIME", "#TIMEL", "#TITLEPARTS", "#USELIQUIDTHREADS", "ANCHORENCODE", "ARTICLEPATH", "BIDI", "CANONICALURL", "CANONICALURLE", "FILEPATH", "FORMATNUM", "FULLURL", "FULLURLE", "GENDER", "GRAMMAR", "INT", "LC", "LCFIRST", "LOCALURL", "LOCALURLE", "MSG", "MSGNW", "NOEXTERNALLANGLINKS", "NS", "NSE", "PADLEFT", "PADRIGHT", "PAGEID", "PLURAL", "RAW", "SAFESUBST", "SCRIPTPATH", "SERVER", "SERVERNAME", "STYLEPATH", "SUBST", "UC", "UCFIRST", "URLENCODE"} do
		case_insensitive[k] = true
	end
	
	local parser_functions = setmetatable({}, case_insensitive)
	
	parser_functions["#BABEL"] = function(self, args)
		local params = self:get_array_params(args)
		if params == false then
			return self
		end
		return Frame:callParserFunction("#BABEL", params)
	end
	
	parser_functions["#CATEGORYTREE"] = function(self, args)
		-- TODO
	end
	
	parser_functions["#EXPR"] = function(self, args)
		-- TODO
	end
	
	parser_functions["#FORMATDATE"] = function(self, args)
	end
	
	parser_functions["#IF"] = function(self, args)
		local check = trim(self:expand_child(2, args, true))
		if args == false and type(check) == "table" then
			return self
		end
		local n = check ~= "" and 3 or 4
		return trim(self:expand_child(n, args, true)) or ""
	end
	
	parser_functions["#IFEQ"] = function(self, args)
		local check1 = tonumber_loose(trim(self:expand_child(2, args, true))) -- and decode entities
		local check2 = tonumber_loose(trim(self:expand_child(3, args, true))) -- and decode entities
		if args == false and (type(check1) == "table" or type(check2) == "table") then
			return self
		end
		local n = check1 == check2 and 4 or 5
		return trim(self:expand_child(n, args, true)) or ""
	end
	
	do
		local tags = {"strong", "span", "p", "div"}
		
		parser_functions["#IFERROR"] = function(self, args)
			local check = trim(self:expand_child(2, args, true))
			if args == false and type(check) == "table" then
				return self
			end
			for _, tag in ipairs(tags) do
				if match(check, "<" .. tag .. "%s[^>]-%f[^%s]class=\"[^\">]-%f[^%s\"]error%f[%s\"][^\">]-\"") then
					return trim(self:expand_child(3, args, true)) or ""
				end
			end
			if self[4] then
				return trim(self:expand_child(4, args, true))
			end
			return check
		end
	end
	
	parser_functions["#IFEXIST"] = function(self, args)
		local check = trim(self:expand_child(2, args, true))
		if args == false and type(check) == "table" then
			return self
		end
		local title = new_title(check)
		local n = title and title.exists and 3 or 4
		return trim(self:expand_child(n, args, true)) or ""
	end
	
	parser_functions["#IFEXPR"] = function(self, args)
	end
	
	parser_functions["#INVOKE"] = function(self, args)
		self.module = self.module or remove(self, 2)
		if not self.func then
			local func = trim(self:expand_child(2, args, true))
			if type(func) == "table" then
				return self
			end
			self.func = remove(self, 2)
		end
		if args == false then
			return self
		end
		local params = self:get_params(args)
		local parent_args
		if args then
			parent_args = {}
			for k, v in pairs(args) do
				parent_args[k] = v
			end
		end
		parent_frame = Frame:newChild{
			title = self.title.fullText,
			args = parent_args,
			parent = false
		}
		local child_args = {}
		for k, v in pairs(params) do
			child_args[k] = v
		end
		child_frame = parent_frame:newChild{
			title = "Module:" .. self.module,
			args = child_args,
			parent = parent_frame
		}
		return tostring(require("Module:" .. self.module)[self.func](child_frame))
	end
	
	parser_functions["#LANGUAGE"] = function(self, args)
	end
	
	parser_functions["#LQTPAGELIMIT"] = function(self, args)
	end
	
	parser_functions["#LST"] = function(self, args)
	end
	
	parser_functions["#LSTH"] = function(self, args)
	end
	
	parser_functions["#LSTX"] = function(self, args)
	end
	
	parser_functions["#PROPERTY"] = function(self, args)
	end
	
	parser_functions["#REL2ABS"] = function(self, args)
	end
	
	parser_functions["#SPECIAL"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return Frame:callParserFunction("#SPECIAL", params)
	end
	
	parser_functions["#SPECIALE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return Frame:callParserFunction("#SPECIALE", params)
	end
	
	parser_functions["#STATEMENTS"] = function(self, args)
	end
	
	-- Parsoid keeps expanding grouped keys even after it finds a match in a given group, which means they can still cause errors to be thrown; this does not apply to the final key in the group, however. In {{#switch:a|a|b|c=d}}, "b" is expanded (despite "a" already having matched), but "c" is ignored.
	-- When there are duplicate keys, the first takes priority. However, when there are duplicate #defaults, the last takes priority; this include the implied #default in the final parameter if it has no "=" sign.
	-- #default is not case sensitive, and can also be a key: e.g. if #default and #DEFAULT were both used (in that order), #DEFAULT would be the actual default, but #default could still be treated as a key (requiring an exact match).
	parser_functions["#SWITCH"] = function(self, args)
		local check, next_value, default
		self[2] = trim(self[2]) -- and decode entities
		for i = 3, #self do
			if self[i].key then
				if next_value then
					return trim(self:expand_child(i, args))
				end
				check = trim(self[i]:expand_child("key", args)) -- and decode entities
				if check == self[2] then
					return trim(self:expand_child(i, args))
				elseif lower(check) == "#default" then
					default = i
				end
			else
				check = trim(self:expand_child(i, args)) -- and decode entities
				if check == self[2] then
					next_value = true
				elseif i == #self then
					return check -- decode entities
				end
			end
		end
		if default then
			return trim(self:expand_child(default, args))
		end
		return ""
	end
	
	parser_functions["#TAG"] = function(self, args)
	end
	
	parser_functions["#TARGET"] = function(self, args)
	end
	
	parser_functions["#TIME"] = function(self, args)
	end
	
	parser_functions["#TIMEL"] = function(self, args)
	end
	
	parser_functions["#TITLEPARTS"] = function(self, args)
	end
	
	parser_functions["#USELIQUIDTHREADS"] = function(self, args)
	end
	
	parser_functions["ANCHORENCODE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return mw.uri.anchorEncode(params[1])
	end
	
	parser_functions["BASEPAGENAME"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.baseText and
			nowiki(title.baseText) or ""
	end
	
	parser_functions["BASEPAGENAMEE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.baseText and
			nowiki(uri_encode(title.baseText, "WIKI")) or ""
	end
	
	parser_functions["BIDI"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return Frame:callParserFunction("BIDI", params)
	end
	
	parser_functions["CANONICALURL"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return tostring(mw.uri.canonicalUrl(params[1], params[2]))
	end
	
	parser_functions["CANONICALURLE"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return uri_encode(tostring(mw.uri.canonicalUrl(params[1], params[2])), "WIKI")
	end
	
	parser_functions["CASCADINGSOURCES"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.cascadingProtection and
			type(title.cascadingProtection.sources) == "table" and
			concat(title.cascadingProtection.sources, "|") or ""
	end
	
	parser_functions["DEFAULTSORT"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return Frame:callParserFunction("DEFAULTSORT", params)
	end
	
	parser_functions["DISPLAYTITLE"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return Frame:callParserFunction("DISPLAYTITLE", params)
	end
	
	parser_functions["FILEPATH"] = function(self, args)
		local params = self:get_array_params(args, 3)
		if params == false then
			return self
		end
		return Frame:callParserFunction("FILEPATH", params)
	end
	
	parser_functions["FORMATNUM"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return getPageLanguage():formatNum(params[1], params[2])
	end
	
	parser_functions["FULLPAGENAME"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.prefixedText and
			nowiki(title.prefixedText) or ""
	end
	
	parser_functions["FULLPAGENAMEE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.prefixedText and
			nowiki(uri_encode(title.prefixedText, "WIKI")) or ""
	end
	
	parser_functions["FULLURL"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return tostring(mw.uri.fullUrl(params[1], params[2]))
	end
	
	parser_functions["FULLURLE"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return uri_encode(tostring(mw.uri.fullUrl(params[1], params[2])), "WIKI")
	end
	
	parser_functions["GENDER"] = function(self, args)
	end
	
	parser_functions["GRAMMAR"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return getPageLanguage():grammar(params[1], params[2])
	end
	
	parser_functions["INT"] = function(self, args)
	end
	
	parser_functions["LC"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return getContentLanguage():lc(params[1])
	end
	
	parser_functions["LCFIRST"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return getContentLanguage():lcfirst(params[1])
	end
	
	parser_functions["LOCALURL"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return tostring(mw.uri.localUrl(params[1], params[2]))
	end
	
	parser_functions["LOCALURLE"] = function(self, args)
		local params = self:get_array_params(args, 2)
		if params == false then
			return self
		end
		return uri_encode(tostring(mw.uri.localUrl(params[1], params[2])), "WIKI")
	end
	
	parser_functions["NAMESPACE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.nsText and
			nowiki(title.nsText) or ""
	end
	
	parser_functions["NAMESPACEE"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.nsText and
			nowiki(uri_encode(title.nsText, "WIKI")) or ""
	end
	
	parser_functions["NAMESPACENUMBER"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		local title = new_title(params[1])
		return title and
			title.namespace and
			tostring(title.namespace) or ""
	end
	
	parser_functions["NOEXTERNALLANGLINKS"] = function(self, args)
	end
	
	parser_functions["NS"] = function(self, args)
	end
	
	parser_functions["NSE"] = function(self, args)
	end
	
	parser_functions["NUMBERINGROUP"] = function(self, args)
	end
	
	parser_functions["NUMBEROFACTIVEUSERS"] = function(self, args)
	end
	
	parser_functions["NUMBEROFADMINS"] = function(self, args)
	end
	
	parser_functions["NUMBEROFARTICLES"] = function(self, args)
	end
	
	parser_functions["NUMBEROFEDITS"] = function(self, args)
	end
	
	parser_functions["NUMBEROFFILES"] = function(self, args)
	end
	
	parser_functions["NUMBEROFPAGES"] = function(self, args)
	end
	
	parser_functions["NUMBEROFUSERS"] = function(self, args)
	end
	
	do
		local function pad(str, len, padding)
			if not len or padding == "" then
				return ""
			end
			len = tonumber_strict(len)
			if type(len) ~= "number" or len < 1 then
				return ""
			elseif not padding then
				padding = "0"
			end
			local padding_len = ulen(padding)
			local ret_len = min(len, 500) - ulen(str)
			if ret_len <= 0 then
				return ""
			end
			return rep(padding, floor(ret_len / padding_len)) ..
				sub(padding, 1, ret_len % padding_len)
		end
		
		parser_functions["PADLEFT"] = function(self, args)
			local params = self:get_array_params(args, 3)
			if params == false then
				return self
			end
			return pad(params[1], params[2], params[3]) .. params[1]
		end
		
		parser_functions["PADRIGHT"] = function(self, args)
			local params = self:get_array_params(args, 3)
			if params == false then
				return self
			end
			return params[1] .. pad(params[1], params[2], params[3])
		end
	end
	
	parser_functions["PAGEID"] = function(self, args)
	end
	
	parser_functions["PAGENAME"] = function(self, args)
	end
	
	parser_functions["PAGENAMEE"] = function(self, args)
	end
	
	parser_functions["PAGESINCATEGORY"] = function(self, args)
	end
	
	parser_functions["PAGESIZE"] = function(self, args)
	end
	
	parser_functions["PLURAL"] = function(self, args)
	end
	
	parser_functions["PROTECTIONEXPIRY"] = function(self, args)
	end
	
	parser_functions["PROTECTIONLEVEL"] = function(self, args)
	end
	
	parser_functions["REVISIONDAY"] = function(self, args)
	end
	
	parser_functions["REVISIONDAY2"] = function(self, args)
	end
	
	parser_functions["REVISIONID"] = function(self, args)
	end
	
	parser_functions["REVISIONMONTH"] = function(self, args)
	end
	
	parser_functions["REVISIONMONTH1"] = function(self, args)
	end
	
	parser_functions["REVISIONTIMESTAMP"] = function(self, args)
	end
	
	parser_functions["REVISIONUSER"] = function(self, args)
	end
	
	parser_functions["REVISIONYEAR"] = function(self, args)
	end
	
	parser_functions["ROOTPAGENAME"] = function(self, args)
	end
	
	parser_functions["ROOTPAGENAMEE"] = function(self, args)
	end
	
	parser_functions["SUBJECTPAGENAME"] = function(self, args)
	end
	
	parser_functions["SUBJECTPAGENAMEE"] = function(self, args)
	end
	
	parser_functions["SUBJECTSPACE"] = function(self, args)
	end
	
	parser_functions["SUBJECTSPACEE"] = function(self, args)
	end
	
	parser_functions["SUBPAGENAME"] = function(self, args)
	end
	
	parser_functions["SUBPAGENAMEE"] = function(self, args)
	end
	
	parser_functions["TALKPAGENAME"] = function(self, args)
	end
	
	parser_functions["TALKPAGENAMEE"] = function(self, args)
	end
	
	parser_functions["TALKSPACE"] = function(self, args)
	end
	
	parser_functions["TALKSPACEE"] = function(self, args)
	end
	
	parser_functions["UC"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return getContentLanguage():uc(params[1])
	end
	
	parser_functions["UCFIRST"] = function(self, args)
		local params = self:get_array_params(args, 1)
		if params == false then
			return self
		end
		return getContentLanguage():ucfirst(params[1])
	end
	
	parser_functions["URLENCODE"] = function(self, args)
	end
	
	parser_functions["#DATEFORMAT"] = parser_functions["#FORMATDATE"]
	parser_functions["#SECTION"] = parser_functions["#LST"]
	parser_functions["#SECTION-H"] = parser_functions["#LSTH"]
	parser_functions["#SECTION-X"] = parser_functions["#LSTX"]
	parser_functions["ARTICLEPAGENAME"] = parser_functions["SUBJECTPAGENAME"]
	parser_functions["ARTICLEPAGENAMEE"] = parser_functions["SUBJECTPAGENAMEE"]
	parser_functions["ARTICLESPACE"] = parser_functions["SUBJECTSPACE"]
	parser_functions["ARTICLESPACEE"] = parser_functions["SUBJECTSPACEE"]
	parser_functions["DEFAULTCATEGORYSORT"] = parser_functions["DEFAULTSORT"]
	parser_functions["DEFAULTSORTKEY"] = parser_functions["DEFAULTSORT"]
	parser_functions["NUMINGROUP"] = parser_functions["NUMBERINGROUP"]
	parser_functions["PAGESINCAT"] = parser_functions["PAGESINCATEGORY"]
	
	local parser_variables = setmetatable({}, case_insensitive)
	
	parser_variables["!"] = function()
		return "|"
	end
	
	parser_variables["="] = function()
		return "="
	end
	
	parser_variables["ARTICLEPATH"] = function()
		return new_title("$1"):localUrl()
	end
	
	parser_variables["BASEPAGENAME"] = function()
		return nowiki(getCurrentTitle().baseText)
	end
	
	parser_variables["BASEPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().baseText, "WIKI"))
	end
	
	parser_variables["CASCADINGSOURCES"] = function()
		return concat(getCurrentTitle().cascadingProtection.sources, "|")
	end
	
	parser_variables["CONTENTLANGUAGE"] = function()
		return getContentLanguage():getCode()
	end
	
	parser_variables["CURRENTDAY"] = function()
		return getPageLanguage():formatDate("j")
	end
	
	parser_variables["CURRENTDAY2"] = function()
		return getPageLanguage():formatDate("d")
	end
	
	parser_variables["CURRENTDAYNAME"] = function()
		return getPageLanguage():formatDate("l")
	end
	
	parser_variables["CURRENTDOW"] = function()
		return getPageLanguage():formatDate("w")
	end
	
	parser_variables["CURRENTHOUR"] = function()
		return getPageLanguage():formatDate("H")
	end
	
	parser_variables["CURRENTMONTH"] = function()
		return getPageLanguage():formatDate("m")
	end
	
	parser_variables["CURRENTMONTH1"] = function()
		return getPageLanguage():formatDate("n")
	end
	
	parser_variables["CURRENTMONTHABBREV"] = function()
		return getPageLanguage():formatDate("M")
	end
	
	parser_variables["CURRENTMONTHNAME"] = function()
		return getPageLanguage():formatDate("F")
	end
	
	parser_variables["CURRENTMONTHNAMEGEN"] = function()
		return getPageLanguage():formatDate("xg")
	end
	
	parser_variables["CURRENTTIME"] = function()
		return getPageLanguage():formatDate("H:i")
	end
	
	parser_variables["CURRENTTIMESTAMP"] = function()
		return getPageLanguage():formatDate("YmdHis")
	end
	
	parser_variables["CURRENTVERSION"] = function()
		return mw.site.currentVersion
	end
	
	parser_variables["CURRENTWEEK"] = function()
		return format("%d", getPageLanguage():formatDate("W"))
	end
	
	parser_variables["CURRENTYEAR"] = function()
		return getPageLanguage():formatDate("Y")
	end
	
	parser_variables["DIRECTIONMARK"] = function()
		return getPageLanguage():getDirMark()
	end
	
	parser_variables["FULLPAGENAME"] = function()
		return nowiki(getCurrentTitle().prefixedText)
	end
	
	parser_variables["FULLPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().prefixedText, "WIKI"))
	end
	
	parser_variables["LOCALDAY"] = function()
		return getPageLanguage():formatDate("j", nil, true)
	end
	
	parser_variables["LOCALDAY2"] = function()
		return getPageLanguage():formatDate("d", nil, true)
	end
	
	parser_variables["LOCALDAYNAME"] = function()
		return getPageLanguage():formatDate("l", nil, true)
	end
	
	parser_variables["LOCALDOW"] = function()
		return getPageLanguage():formatDate("w", nil, true)
	end
	
	parser_variables["LOCALHOUR"] = function()
		return getPageLanguage():formatDate("H", nil, true)
	end
	
	parser_variables["LOCALMONTH"] = function()
		return getPageLanguage():formatDate("m", nil, true)
	end
	
	parser_variables["LOCALMONTH1"] = function()
		return getPageLanguage():formatDate("n", nil, true)
	end
	
	parser_variables["LOCALMONTHABBREV"] = function()
		return getPageLanguage():formatDate("M", nil, true)
	end
	
	parser_variables["LOCALMONTHNAME"] = function()
		return getPageLanguage():formatDate("F", nil, true)
	end
	
	parser_variables["LOCALMONTHNAMEGEN"] = function()
		return getPageLanguage():formatDate("xg", nil, true)
	end
	
	parser_variables["LOCALTIME"] = function()
		return getPageLanguage():formatDate("H:i", nil, true)
	end
	
	parser_variables["LOCALTIMESTAMP"] = function()
		return getPageLanguage():formatDate("YmdHis", nil, true)
	end
	
	parser_variables["LOCALWEEK"] = function()
		return format("%d", getPageLanguage():formatDate("W", nil, true))
	end
	
	parser_variables["LOCALYEAR"] = function()
		return getPageLanguage():formatDate("Y", nil, true)
	end
	
	parser_variables["NAMESPACE"] = function()
		return (gsub(getCurrentTitle().nsText, "_", " "))
	end
	
	parser_variables["NAMESPACEE"] = function()
		return uri_encode(getCurrentTitle().nsText, "WIKI")
	end
	
	parser_variables["NAMESPACENUMBER"] = function()
		return tostring(getCurrentTitle().namespace)
	end
	
	parser_variables["NOEXTERNALLANGLINKS"] = function()
		return Frame:callParserFunction("NOEXTERNALLANGLINKS", "*")
	end
	
	parser_variables["NUMBEROFACTIVEUSERS"] = function()
		return getPageLanguage():formatNum(mw.site.stats.activeUsers)
	end
	
	parser_variables["NUMBEROFADMINS"] = function()
		return getPageLanguage():formatNum(mw.site.stats.admins)
	end
	
	parser_variables["NUMBEROFARTICLES"] = function()
		return getPageLanguage():formatNum(mw.site.stats.articles)
	end
	
	parser_variables["NUMBEROFEDITS"] = function()
		return getPageLanguage():formatNum(mw.site.stats.edits)
	end
	
	parser_variables["NUMBEROFFILES"] = function()
		return getPageLanguage():formatNum(mw.site.stats.files)
	end
	
	parser_variables["NUMBEROFPAGES"] = function()
		return getPageLanguage():formatNum(mw.site.stats.pages)
	end
	
	parser_variables["NUMBEROFUSERS"] = function()
		return getPageLanguage():formatNum(mw.site.stats.users)
	end
	
	parser_variables["PAGEID"] = function()
		return tostring(getCurrentTitle().id)
	end
	
	parser_variables["PAGELANGUAGE"] = function()
		return getPageLanguage():getCode()
	end
	
	parser_variables["PAGENAME"] = function()
		return nowiki(getCurrentTitle().text)
	end
	
	parser_variables["PAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().text, "WIKI"))
	end
	
	parser_variables["REVISIONDAY"] = function()
		return Frame:really_preprocess("{{REVISIONDAY}}")
	end
	
	parser_variables["REVISIONDAY2"] = function()
		return Frame:really_preprocess("{{REVISIONDAY2}}")
	end
	
	parser_variables["REVISIONID"] = function()
		return Frame:really_preprocess("{{REVISIONID}}")
	end
	
	parser_variables["REVISIONMONTH"] = function()
		return Frame:really_preprocess("{{REVISIONMONTH}}")
	end
	
	parser_variables["REVISIONMONTH1"] = function()
		return Frame:really_preprocess("{{REVISIONMONTH1}}")
	end
	
	parser_variables["REVISIONSIZE"] = function()
		return Frame:really_preprocess("{{REVISIONSIZE}}")
	end
	
	parser_variables["REVISIONTIMESTAMP"] = function()
		return Frame:really_preprocess("{{REVISIONTIMESTAMP}}")
	end
	
	parser_variables["REVISIONUSER"] = function()
		return Frame:really_preprocess("{{REVISIONUSER}}")
	end
	
	parser_variables["REVISIONYEAR"] = function()
		return Frame:really_preprocess("{{REVISIONYEAR}}")
	end
	
	parser_variables["ROOTPAGENAME"] = function()
		return nowiki(getCurrentTitle().rootText)
	end
	
	parser_variables["ROOTPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().rootText, "WIKI"))
	end
	
	parser_variables["SCRIPTPATH"] = function()
		return mw.site.scriptPath
	end
	
	parser_variables["SERVER"] = function()
		return mw.site.server
	end
	
	parser_variables["SERVERNAME"] = function()
		return Frame:really_preprocess("{{SERVERNAME}}")
	end
	
	parser_variables["SITENAME"] = function()
		return mw.site.siteName
	end
	
	parser_variables["STYLEPATH"] = function()
		return mw.site.stylePath
	end
	
	parser_variables["SUBJECTPAGENAME"] = function()
		return nowiki(getCurrentTitle().subjectPageTitle.fullText)
	end
	
	parser_variables["SUBJECTPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().subjectPageTitle.fullText, "WIKI"))
	end
	
	parser_variables["SUBJECTSPACE"] = function()
		return (gsub(getCurrentTitle().subjectNsText, "_", " "))
	end
	
	parser_variables["SUBJECTSPACEE"] = function()
		return uri_encode(getCurrentTitle().subjectNsText, "WIKI")
	end
	
	parser_variables["SUBPAGENAME"] = function()
		return nowiki(getCurrentTitle().subpageText)
	end
	
	parser_variables["SUBPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().subpageText, "WIKI"))
	end
	
	parser_variables["TALKPAGENAME"] = function()
		return nowiki(getCurrentTitle().talkPageTitle.fullText)
	end
	
	parser_variables["TALKPAGENAMEE"] = function()
		return nowiki(uri_encode(getCurrentTitle().talkPageTitle.fullText, "WIKI"))
	end
	
	parser_variables["TALKSPACE"] = function()
		return (gsub(mw.site.namespaces[getCurrentTitle().namespace].talk.canonicalName, "_", " "))
	end
	
	parser_variables["TALKSPACEE"] = function()
		return uri_encode(mw.site.namespaces[getCurrentTitle().namespace].talk.canonicalName, "WIKI")
	end
	
	parser_variables["ARTICLEPAGENAME"] = parser_variables["SUBJECTPAGENAME"]
	parser_variables["ARTICLEPAGENAMEE"] = parser_variables["SUBJECTPAGENAMEE"]
	parser_variables["ARTICLESPACE"] = parser_variables["SUBJECTSPACE"]
	parser_variables["ARTICLESPACEE"] = parser_variables["SUBJECTSPACEE"]
	parser_variables["CONTENTLANG"] = parser_variables["CONTENTLANGUAGE"]
	parser_variables["CURRENTMONTH2"] = parser_variables["CURRENTMONTH"]
	parser_variables["DIRMARK"] = parser_variables["DIRECTIONMARK"]
	parser_variables["LOCALMONTH2"] = parser_variables["LOCALMONTH"]
	
	local transclusion_modifiers = setmetatable({}, case_insensitive)
	
	transclusion_modifiers["MSG"] = function(self)
	end
	
	transclusion_modifiers["MSGNW"] = function(self)
	end
	
	transclusion_modifiers["RAW"] = function(self)
	end
	
	transclusion_modifiers["SAFESUBST"] = function(self)
	end
	
	transclusion_modifiers["SUBST"] = function(self)
	end
	
	local templates = {}
	
	function Template:expand(args)
		local name = self:expand_child(1, args)
		if type(name) == "table" then
			return self
		end
		for snippet, nxt_loc in gmatch(name, "([^:]*):()") do
			snippet = trim(snippet)
			if transclusion_modifiers[snippet] then
				transclusion_modifiers[snippet](self)
			elseif parser_functions[snippet] then
				if not self.colon then
					insert(self, 2, sub(name, nxt_loc))
					self.colon = true
				end
				return parser_functions[snippet](self, args)
			end
		end
		name = trim(name)
		if #self == 1 and parser_variables[name] then
			return parser_variables[name](self, args)
		end
		local params = self:get_params(args)
		local title = new_title(name, 10)
		title = title.redirectTarget or title
		local title_text = title.fullText
		if not templates[title_text] then
			templates[title_text] = export.parse(title:getContent(), title, true):expand(false)
			pcall(rawset, templates[title_text], "cached", true)
		end
		if type(templates[title_text]) ~= "table" then
			return templates[title_text]
		elseif params == false then
			return self
		end
		return templates[title_text]:expand(params)
	end
end

------------------------------------------------------------------------------------
--
-- Parser
--
------------------------------------------------------------------------------------

local Parser = m_parser.Parser

-- Argument.
do
	local function handle_argument(self, this)
		if this == "|" then
			self:emit(Wikitext:new(self:pop_sublayer()))
			self:push_sublayer()
		elseif this == "}" and self:read(1) == "}" then
			if self:read(2) == "}" then
				self:emit(Wikitext:new(self:pop_sublayer()))
				self:advance(2)
				return self:pop()
			end
			return self:fail_route()
		elseif this == "" then
			return self:fail_route()
		else
			return self:block_handler(this)
		end
	end

	function Parser:argument()
		local argument = self:get(handle_argument, self.push_sublayer)
		if argument == self.bad_route then
			self:template()
		else
			if #self:layer() == self.emit_pos then
				local inner = self:remove()
				if type(argument[1]) == "table" then
					insert(argument[1], 1, inner)
				else
					argument[1] = Wikitext:new{inner, argument[1]}
				end
			end
			self.braces = self.braces - 3
			self.brace_head = self.brace_head - 3
			argument.pos = self.brace_head
			self:emit(Argument:new(argument))
		end
	end
end

-- Template.
do
	local handle_name
	local handle_parameter
	
	function handle_name(self, this)
		if this == "|" then
			self:emit(Wikitext:new(self:pop_sublayer()))
			self.handler = handle_parameter
			self:push_sublayer()
		elseif this == "}" and self:read(1) == "}" then
			self:emit(Wikitext:new(self:pop_sublayer()))
			self:advance()
			return self:pop()
		elseif this == "" then
			return self:fail_route()
		else
			return self:block_handler(this)
		end
	end
	
	function handle_parameter(self, this)
		if this == "=" and not self.key and (
			self:read(1) ~= "=" or
			self:read(-1) ~= "\n" and self:read(-1) ~= ""
		) then
			local key = self:pop_sublayer()
			self:push_sublayer()
			rawset(self:layer(), "key", Wikitext:new(key))
		elseif this == "|" then
			self:emit(Parameter:new(self:pop_sublayer()))
			self:push_sublayer()
		elseif this == "}" and self:read(1) == "}" then
			self:emit(Parameter:new(self:pop_sublayer()))
			self:advance()
			return self:pop()
		elseif this == "" then
			return self:fail_route()
		else
			return self:block_handler(this)
		end
	end
	
	function Parser:template()
		local template = self:get(handle_name, self.push_sublayer)
		if template == self.bad_route then
			self:advance(-1)
			for _ = 1, self.braces do
				self:emit(self.emit_pos, "{")
			end
			self.braces = 0
		else
			if #self:layer() == self.emit_pos then
				local inner = self:remove()
				if type(template[1]) == "table" then
					insert(template[1], 1, inner)
				else
					template[1] = Wikitext:new{inner, template[1]}
				end
			end
			self.braces = self.braces - 2
			self.brace_head = self.brace_head - 2
			template.pos = self.brace_head
			self:emit(Template:new(template))
		end
	end
	
	function Parser:template_or_argument()
		self:advance(2)
		self.braces = 2
		while self:read() == "{" do
			self:advance()
			self.braces = self.braces + 1
		end
		self.emit_pos = #self:layer() + 1
		self.brace_head = self.raw_head
		repeat
			if self.braces == 1 then
				self:emit(self.emit_pos, "{")
				break
			elseif self.braces == 2 then
				self:template()
			else
				self:argument()
			end
			self:advance()
		until self.braces == 0
		self:advance(-1)
	end
end

-- Text not in <onlyinclude></onlyinclude>.
function Parser:not_onlyinclude()
	local this, nxt, nxt2 = self:read(0, 1, 2)
	while not (
		this == "" or
		this == "<" and nxt == "onlyinclude" and nxt2 == ">"
	) do
		self:advance()
		this, nxt, nxt2 = nxt, nxt2, self:read(2)
	end
	self:advance(2)
end

-- Tag.
do
	local function is_ignored_tag(self, check)
		return self.transcluded and check == "includeonly" or
			not self.transcluded and (
				check == "noinclude" or
				check == "onlyinclude"
			)
	end
	
	-- Handlers.
	local handle_start
	local handle_ignored_tag_start
	local handle_ignored_tag
	local handle_after_tag_name
	local handle_before_attribute_name
	local handle_attribute_name
	local handle_before_attribute_value
	local handle_quoted_attribute_value
	local handle_unquoted_attribute_value
	local handle_after_attribute_value
	local handle_tag_block
	local handle_end
	
	function handle_start(self, this)
		if this == "/" then
			local check = lower(self:read(1))
			if is_ignored_tag(self, check) then
				self.name = check
				self.ignored = true
				self:advance()
				self.handler = handle_ignored_tag_start
				return
			end
			return self:fail_route()
		end
		local check = lower(this)
		if is_ignored_tag(self, check) then
			self.name = check
			self.ignored = true
			self.handler = handle_ignored_tag_start
		elseif (
			check == "noinclude" and self.transcluded or
			check == "includeonly" and not self.transcluded
		) then
			self.name = check
			self.ignored = true
			self.handler = handle_after_tag_name
		elseif TAGS[check] then
			self.name = check
			self.handler = handle_after_tag_name
		else
			return self:fail_route()
		end
	end
	
	function handle_ignored_tag_start(self, this)
		if this == ">" then
			return self:pop()
		elseif this == "/" and self:read(1) == ">" then
			self.self_closing = true
			self:advance()
			return self:pop()
		elseif is_space(this) then
			self.handler = handle_ignored_tag
		else
			return self:fail_route()
		end
	end
	
	function handle_ignored_tag(self, this)
		if this == ">" then
			return self:pop()
		elseif this == "" then
			return self:fail_route()
		end
	end
	
	function handle_after_tag_name(self, this)
		if this == "/" and self:read(1) == ">" then
			self.self_closing = true
			self:advance()
			return self:pop()
		elseif this == ">" then
			self.handler = handle_tag_block
		elseif is_space(this) then
			self.handler = handle_before_attribute_name
		else
			return self:fail_route()
		end
	end
	
	function handle_before_attribute_name(self, this)
		if this == "/" and self:read(1) == ">" then
			self.self_closing = true
			self:advance()
			return self:pop()
		elseif this == ">" then
			self.handler = handle_tag_block
		elseif this ~= "/" and not is_space(this) then
			self:push_sublayer(handle_attribute_name)
			return self:consume()
		elseif this == "" then
			return self:fail_route()
		end
	end
	
	function handle_attribute_name(self, this)
		if this == "/" or this == ">" or is_space(this) then
			self:pop_sublayer()
			return self:consume()
		elseif this == "=" then
			self.attr_name = ulower(concat(self:pop_sublayer()))
			self.handler = handle_before_attribute_value
		elseif this == "" then
			return self:fail_route()
		else
			self:emit(this)
		end
	end
	
	function handle_before_attribute_value(self, this)
		if this == "/" or this == ">" then
			handle_after_attribute_value(self, "")
			return self:consume()
		elseif is_space(this) then
			handle_after_attribute_value(self, "")
		elseif this == "\"" or this == "'" then
			self:push_sublayer(handle_quoted_attribute_value)
			rawset(self:layer(), "quoter", this)
		elseif this == "" then
			return self:fail_route()
		else
			self:push_sublayer(handle_unquoted_attribute_value)
			return self:consume()
		end
	end
	
	function handle_quoted_attribute_value(self, this)
		if this == ">" then
			handle_after_attribute_value(self, concat(self:pop_sublayer()))
			return self:consume()
		elseif this == self.quoter then
			handle_after_attribute_value(self, concat(self:pop_sublayer()))
		elseif this == "" then
			return self:fail_route()
		else
			self:emit(this)
		end
	end
			
	function handle_unquoted_attribute_value(self, this)
		if this == "/" or this == ">" then
			handle_after_attribute_value(self, concat(self:pop_sublayer()))
			return self:consume()
		elseif is_space(this) then
			handle_after_attribute_value(self, concat(self:pop_sublayer()))
		elseif this == "" then
			return self:fail_route()
		else
			self:emit(this)
		end
	end
	
	function handle_after_attribute_value(self, attr_value)
		self.attributes = self.attributes or {}
		self.attributes[self.attr_name] = attr_value
		self.attr_name = nil
		self.handler = handle_before_attribute_name
	end
	
	function handle_tag_block(self, this)
		if (
			this == "<" and
			self:read(1) == "/" and
			lower(self:read(2)) == self.name
		) then
			local tag_end = self:get(handle_end, self.advance, 3)
			if tag_end == self.bad_route then
				self:emit("<")
			else
				return self:pop()
			end
		elseif this == "" then
			return self:fail_route()
		else
			self:emit(this)
		end
	end
	
	function handle_end(self, this)
		if this == ">" then
			return self:pop()
		elseif not is_space(this) then
			return self:fail_route()
		end
	end
	
	function Parser:tag()
		local tag = self:get(handle_start, self.advance)
		if tag == self.bad_route then
			self:emit("<")
		else
			self:emit(Tag:new(tag))
		end
	end
end

-- Block handlers.
do
	local function handle_heading_block(self, this)
		if this == "\n" then
			self:emit("\n")
			return self:pop()
		else
			return self:block_handler(this)
		end
	end
	
	local function handle_language_conversion_block(self, this)
		if this == "}" and self:read(1) == "-" then
			self:advance()
			self:emit("}", "-")
			return self:pop()
		else
			return self:block_handler(this)
		end
	end
	
	local function handle_wikilink_block(self, this)
		if this == "]" and self:read(1) == "]" then
			self:advance()
			self:emit("]", "]")
			return self:pop()
		else
			return self:block_handler(this)
		end
	end
	
	function Parser:block_handler(this)
		if this == "-" and self:read(1) == "{" then
			self:advance()
			self:emit("-")
			if self:read(1) == "{" then
				self:template_or_argument()
			else
				self:emit_tokens(self:get(handle_language_conversion_block))
			end
		elseif this == "=" and (
			self:read(-1) == "\n" or
			self:read(-1) == ""
		) then
			self:advance()
			self:emit("=")
			self:emit_tokens(self:get(handle_heading_block))
		elseif this == "[" and self:read(1) == "[" then
			self:advance()
			self:emit("[")
			self:emit_tokens(self:get(handle_wikilink_block))
		else
			return self:main_handler(this)
		end
	end
end

function Parser:main_handler(this)
	if this == "<" then
		 if (
			self:read(1) == "!" and
			self:read(2) == "-" and
			self:read(3) == "-"
		 ) then
			self:advance(4)
			local this, nxt, nxt2 = self:read(0, 1, 2)
			while not (
				this == "" or
				this == "-" and nxt == "-" and nxt2 == ">"
			) do
				self:advance()
				this, nxt, nxt2 = nxt, nxt2, self:read(2)
			end
			self:advance(2)
		 elseif (
		 	self.onlyinclude and
		 	self:read(1) == "/" and
		 	self:read(2) == "onlyinclude" and
		 	self:read(3) == ">"
		) then
			self:advance(4)
			self:not_onlyinclude()
		else
			self:tag()
		end
	elseif this == "{" and self:read(1) == "{" then
		self:template_or_argument()
	elseif this == "" then
		return self:pop()
	else
		self:emit(this)
	end
end

do
	local function do_parse(self, str, title, transcluded)
		rawset(self, "title", title)
		if transcluded then
			rawset(self, "transcluded", true)
			if match(str, "<onlyinclude>") and match(str, "</onlyinclude>") then
				rawset(self, "onlyinclude", true)
				self:not_onlyinclude()
				self:advance()
			end
		end
	end
	
	function export.parse(str, title, transcluded)
		local text = {}
		for chunk, char in gmatch(str, "([^%s!\"'%-/<=>%[%]{|}]*)(.?)") do
			if #chunk > 0 then
				insert(text, chunk)
			end
			if #char > 0 then
				insert(text, char)
			end
		end
		local tokens = Parser:parse(
			text,
			Parser.main_handler,
			do_parse,
			str,
			title,
			transcluded
		)
		return tokens
	end
end

function export.parse_page(title)
	local title = type(title) == "string" and title or title.args[1]
	mw.title.getCurrentTitle = function()
		return mw.title.new(title)
	end
	return export.parse(mw.title.getCurrentTitle():getContent()):expand()
end

return export