Module:Transcluder
-- Module:Transcluder is a general-purpose transclusion engine
 -- Documentation and master version: https://en.wikipedia.org/wiki/Module:Transcluder
 -- Authors: User:Sophivorus, User:Certes &amp; others
 -- License: CC-BY-SA-3.0
 
 local p = {}
 
 -- Helper function to test for truthy and falsy values
 -- @todo Somehow internationalize it
 local function truthy(value)
 	if not value or value == '' or value == 0 or value == '0' or value == 'false' or value == 'no' or value == 'non' then
 		return false
 	end
 	return true
 end
 
 -- Helper function to match from a list of regular expressions
 -- Like so: match pre..list[1]..post or pre..list[2]..post or ...
 local function matchAny(text, pre, list, post, init)
 	local match = {}
 	for i = 1, #list do
 		match = { mw.ustring.match(text, pre .. list[i] .. post, init) }
 		if match[1] then return unpack(match) end
 	end
 	return nil
 end
 
 -- Like matchAny but for Category/File links with less overhead
 local function matchAnyLink(text, list)
 	local match
 	for _, v in ipairs(list) do
 		match = string.match(text, '%[%[%s*' .. v .. '%s*:.*%]%]')
 		if match then break end
 	end
 	return match
 end
 
 -- Helper function to escape a string for use in regexes
 local function escapeString(str)
 	return string.gsub(str, '[%^%$%(%)%.%[%]%*%+%-%?%%]', '%%%0')
 end
 
 -- Helper function to remove a string from a text
 local function removeString(text, str)
 	local pattern = escapeString(str)
 	if #pattern &gt; 9999 then -- strings longer than 10000 bytes can't be put into regexes
 		pattern = escapeString(mw.ustring.sub(str, 1, 999)) .. '.-' .. escapeString(mw.ustring.sub(str, -999))
 	end
 	return string.gsub(text, pattern, '')
 end
 
 -- Helper function to convert a comma-separated list of numbers or min-max ranges into a list of booleans
 -- @param flags Comma-separated list of numbers or min-max ranges, for example '1,3-5'
 -- @return Map from integers to booleans, for example {1=true,2=false,3=true,4=true,5=true}
 -- @return Boolean indicating whether the flags should be treated as a blacklist or not
 local function parseFlags(value)
 	local flags = {}
 	local blacklist = false
 
 	if not value then return nil, false end
 
 	if type(value) == 'number' then
 		if value &lt; 0 then
 			value = -value
 			blacklist = true
 		end
 		flags = { [value] = true }
 
 	elseif type(value) == 'string' then
 		if string.sub(value, 1, 1) == '-' then
 			blacklist = true
 			value = string.sub(value, 2)
 		end
 		local ranges = mw.text.split(value, ',') -- split ranges: '1,3-5' to {'1','3-5'}
 		for _, range in pairs(ranges) do
 			range = mw.text.trim(range)
 			local min, max = mw.ustring.match(range, '^(%d+)%s*[-–—]%s*(%d+)$') -- '3-5' to min=3 max=5
 			if not max then min, max = string.match(range, '^((%d+))$') end -- '1' to min=1 max=1
 			if max then
 				for i = min, max do flags[i] = true end
 			else
 				flags[range] = true -- if we reach this point, the string had the form 'a,b,c' rather than '1,2,3'
 			end
 		end
 
 	-- List has the form { [1] = false, [2] = true, ['c'] = false }
 	-- Convert it to { [1] = true, [2] = true, ['c'] = true }
 	-- But if ANY value is set to false, treat the list as a blacklist
 	elseif type(value) == 'table' then
 		for i, v in pairs(value) do
 			if v == false then blacklist = true end
 			flags[i] = true
 		end
 	end
 
 	return flags, blacklist
 end
 
 -- Helper function to see if a value matches any of the given flags
 local function matchFlag(value, flags)
 	if not value then return false end
 	value = tostring(value)
 	local lang = mw.language.getContentLanguage()
 	local lcvalue = lang:lcfirst(value)
 	local ucvalue = lang:ucfirst(value)
 	for flag in pairs(flags) do
 		if value == tostring(flag)
 		or lcvalue == flag
 		or ucvalue == flag
 		or ( not tonumber(flag) and mw.ustring.match(value, flag) ) then
 			return true
 		end
 	end
 end
 
 -- Helper function to convert template arguments into an array of options fit for get()
 local function parseArgs(frame)
 	local args = {}
 	for key, value in pairs(frame:getParent().args) do args[key] = value end
 	for key, value in pairs(frame.args) do args[key] = value end -- args from Lua calls have priority over parent args from template
 	return args
 end
 
 -- Error handling function
 -- Throws a Lua error or returns an empty string if error reporting is disabled
 local function throwError(key, value)
 	local TNT = require('Module:TNT')
 	local ok, message = pcall(TNT.format, 'I18n/Module:Transcluder.tab', 'error-' .. key, value)
 	if not ok then message = key end
 	error(message, 2)
 end
 
 -- Error handling function
 -- Returns a wiki friendly error or an empty string if error reporting is disabled
 local function getError(key, value)
 	local TNT = require('Module:TNT')
 	local ok, message = pcall(TNT.format, 'I18n/Module:Transcluder.tab', 'error-' .. key, value)
 	if not ok then message = key end
 	message = mw.html.create('div'):addClass('error'):wikitext(message)
 	return message
 end
 
 -- Helper function to get the local name of a namespace and all its aliases
 -- @param name Canonical name of the namespace, for example 'File'
 -- @return Local name of the namespace and all aliases, for example {'File','Image','Archivo','Imagen'}
 local function getNamespaces(name)
 	local namespaces = mw.clone(mw.site.namespaces[name].aliases) -- Clone because https://en.wikipedia.org/w/index.php?diff=1056921358
 	table.insert(namespaces, mw.site.namespaces[name].name)
 	table.insert(namespaces, mw.site.namespaces[name].canonicalName)
 	return namespaces
 end
 
 -- Get the page wikitext, following redirects
 -- Also returns the page name, or the target page name if a redirect was followed, or false if no page was found
 -- For file pages, returns the content of the file description page
 local function getText(page, noFollow)
 	local title = mw.title.new(page)
 	if not title then return false, false end
 
 	local target = title.redirectTarget
 	if target and not noFollow then title = target end
 
 	local text = title:getContent()
 	if not text then return false, title.prefixedText end
 
 	-- Remove &lt;noinclude&gt; tags
 	text = string.gsub(text, '&lt;[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]&gt;.-&lt;/[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]&gt;', '') -- remove noinclude bits
 
 	-- Keep &lt;onlyinclude&gt; tags
 	if string.find(text, 'onlyinclude') then -- avoid expensive search if possible
 		text = text
 			:gsub('&lt;/onlyinclude&gt;.-&lt;onlyinclude&gt;', '') -- remove text between onlyinclude sections
 			:gsub('^.-&lt;onlyinclude&gt;', '') -- remove text before first onlyinclude section
 			:gsub('&lt;/onlyinclude&gt;.*', '') -- remove text after last onlyinclude section
 	end
 
 	return text, title.prefixedText
 end
 
 -- Get the requested files from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of files to return, for example 2 or '1,3-5'. Omit to return all files.
 -- @return Sequence of strings containing the wikitext of the requested files.
 -- @return Original wikitext minus requested files.
 local function getFiles(text, flags)
 	local files = {}
 	local flags, blacklist = parseFlags(flags)
 	local fileNamespaces = getNamespaces('File')
 	local name
 	local count = 0
 	for file in string.gmatch(text, '%b[]') do
 		if matchAnyLink(file, fileNamespaces) then
 			name = string.match(file, '%[%[[^:]-:([^]|]+)')
 			count = count + 1
 			if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
 			or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
 				table.insert(files, file)
 			else
 				text = removeString(text, file)
 			end
 		end
 	end
 
 	return files, text
 end
 
 -- Get the requested tables from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of tables to return, for example 2 or '1,3-5'. Omit to return all tables.
 -- @return Sequence of strings containing the wikitext of the requested tables.
 -- @return Original wikitext minus requested tables.
 local function getTables(text, flags)
 	local tables = {}
 	local flags, blacklist = parseFlags(flags)
 	local id
 	local count = 0
 	for t in string.gmatch('\n' .. text, '\n%b{}') do
 		if string.sub(t, 1, 3) == '\n{|' then
 			id = string.match(t, '\n{|[^\n]-id%s*=%s*[&quot;\']?([^&quot;\'\n]+)[&quot;\']?[^\n]*\n')
 			count = count + 1
 			if not blacklist and ( not flags or flags[count] or flags[id] )
 			or blacklist and flags and not flags[count] and not flags[id] then
 				table.insert(tables, t)
 			else
 				text = removeString(text, t)
 			end
 		end
 	end
 	return tables, text
 end
 
 -- Get the requested templates from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of templates to return, for example 2 or '1,3-5'. Omit to return all templates.
 -- @return Sequence of strings containing the wikitext of the requested templates.
 -- @return Original wikitext minus requested templates.
 local function getTemplates(text, flags)
 	local templates = {}
 	local flags, blacklist = parseFlags(flags)
 	local name
 	local count = 0
 	for template in string.gmatch(text, '{%b{}}') do
 		if string.sub(template, 1, 3) ~= '{{#' then -- skip parser functions like #if
 			name = mw.text.trim( string.match(template, '{{([^}|\n]+)') or &quot;&quot; ) -- get the template name
 			if name ~= &quot;&quot; then
 				count = count + 1
 				if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
 				or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
 					table.insert(templates, template)
 				else
 					text = removeString(text, template)
 				end
 			end
 		end
 	end
 	return templates, text
 end
 
 -- Get the requested template parameters from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of parameters to return, for example 2 or '1,3-5'. Omit to return all parameters.
 -- @return Map from parameter name to value, NOT IN THE ORIGINAL ORDER
 -- @return Original wikitext minus requested parameters.
 -- @return Order in which the parameters were parsed.
 local function getParameters(text, flags)
 	local parameters, parameterOrder = {}, {}
 	local flags, blacklist = parseFlags(flags)
 	local params, count, parts, key, value
 	for template in string.gmatch(text, '{%b{}}') do
 		params = string.match(template, '{{[^|}]-|(.*)}}')
 		if params then
 			count = 0
 			-- Temporarily replace pipes in subtemplates and links to avoid chaos
 			for subtemplate in string.gmatch(params, '{%b{}}') do
 				params = string.gsub(params, escapeString(subtemplate), string.gsub(subtemplate, &quot;.&quot;, {[&quot;%&quot;]=&quot;%%&quot;, [&quot;|&quot;]=&quot;@@:@@&quot;, [&quot;=&quot;]=&quot;@@_@@&quot;}) )
 			end
 			for link in string.gmatch(params, '%b[]') do
 				params = string.gsub(params, escapeString(link), string.gsub(link, &quot;.&quot;, {[&quot;%&quot;]=&quot;%%&quot;, [&quot;|&quot;]=&quot;@@:@@&quot;, [&quot;=&quot;]=&quot;@@_@@&quot;}) )
 			end
 			for parameter in mw.text.gsplit(params, '|') do
 				parts = mw.text.split(parameter, '=')
 				key = mw.text.trim(parts[1])
 				if #parts == 1 then
 					value = key
 					count = count + 1
 					key = count
 				else
 					value = mw.text.trim(table.concat(parts, '=', 2))
 				end
 				value = string.gsub(string.gsub(value, '@@:@@', '|'), '@@_@@', '=')
 				if not blacklist and ( not flags or matchFlag(key, flags) )
 				or blacklist and flags and not matchFlag(key, flags) then
 					table.insert(parameterOrder, key)
 					parameters[key] = value
 				else
 					text = removeString(text, parameter)
 				end
 			end
 		end
 	end
 	return parameters, text, parameterOrder
 end
 
 -- Get the requested lists from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of lists to return, for example 2 or '1,3-5'. Omit to return all lists.
 -- @return Sequence of strings containing the wikitext of the requested lists.
 -- @return Original wikitext minus requested lists.
 local function getLists(text, flags)
 	local lists = {}
 	local flags, blacklist = parseFlags(flags)
 	local count = 0
 	for list in string.gmatch('\n' .. text .. '\n\n', '\n([*#].-)\n[^*#]') do
 		count = count + 1
 		if not blacklist and ( not flags or flags[count] )
 		or blacklist and flags and not flags[count] then
 			table.insert(lists, list)
 		else
 			text = removeString(text, list)
 		end
 	end
 	return lists, text
 end
 
 -- Get the requested paragraphs from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of paragraphs to return, for example 2 or '1,3-5'. Omit to return all paragraphs.
 -- @return Sequence of strings containing the wikitext of the requested paragraphs.
 -- @return Original wikitext minus requested paragraphs.
 local function getParagraphs(text, flags)
 	local paragraphs = {}
 	local flags, blacklist = parseFlags(flags)
 
 	-- Remove non-paragraphs
 	local elements
 	local temp = '\n' .. text .. '\n'
 	elements, temp = getLists(temp, 0) -- remove lists
 	elements, temp = getFiles(temp, 0) -- remove files
 	temp = mw.text.trim((temp
 		:gsub('\n%b{} *\n', '\n%0\n') -- add spacing between tables and block templates
 		:gsub('\n%b{} *\n', '\n') -- remove tables and block templates
 		:gsub('\n==+[^=]+==+ *\n', '\n') -- remove section titles
 	))
 
 	-- Assume that anything remaining is a paragraph
 	local count = 0
 	for paragraph in mw.text.gsplit(temp, '\n\n+') do
 		if mw.text.trim(paragraph) ~= '' then
 			count = count + 1
 			if not blacklist and ( not flags or flags[count] )
 			or blacklist and flags and not flags[count] then
 				table.insert(paragraphs, paragraph)
 			else
 				text = removeString(text, paragraph)
 			end
 		end
 	end
 
 	return paragraphs, text
 end
 
 -- Get the requested categories from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of categories to return, for example 2 or '1,3-5'. Omit to return all categories.
 -- @return Sequence of strings containing the wikitext of the requested categories.
 -- @return Original wikitext minus requested categories.
 local function getCategories(text, flags)
 	local categories = {}
 	local flags, blacklist = parseFlags(flags)
 	local categoryNamespaces = getNamespaces('Category')
 	local name
 	local count = 0
 	for category in string.gmatch(text, '%b[]') do
 		if matchAnyLink(category, categoryNamespaces) then
 			name = string.match(category, '%[%[[^:]-:([^]|]+)')
 			count = count + 1
 			if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
 			or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
 				table.insert(categories, category)
 			else
 				text = removeString(text, category)
 			end
 		end
 	end
 	return categories, text
 end
 
 -- Get the requested references from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of references to return, for example 2 or '1,3-5'. Omit to return all references.
 -- @return Sequence of strings containing the wikitext of the requested references.
 -- @return Original wikitext minus requested references.
 local function getReferences(text, flags)
 	local references = {}
 
 	-- Remove all references, including citations, when 0 references are requested
 	-- This is kind of hacky but currently necessary because the rest of the code
 	-- doesn't remove citations like &lt;ref name=&quot;Foo&quot; /&gt; if Foo is defined elsewhere
 	if flags and not truthy(flags) then
 		text = string.gsub(text, '&lt;%s*[Rr][Ee][Ff][^&gt;/]*&gt;.-&lt;%s*/%s*[Rr][Ee][Ff]%s*&gt;', '')
 		text = string.gsub(text, '&lt;%s*[Rr][Ee][Ff][^&gt;/]*/%s*&gt;', '')
 		return references, text
 	end
 
 	local flags, blacklist = parseFlags(flags)
 	local name
 	local count = 0
 	for reference in string.gmatch(text, '&lt;%s*[Rr][Ee][Ff][^&gt;/]*&gt;.-&lt;%s*/%s*[Rr][Ee][Ff]%s*&gt;') do
 		name = string.match(reference, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?([^&quot;\'&gt;/]+)[&quot;\']?[^&gt;]*%s*&gt;')
 		count = count + 1
 		if not blacklist and ( not flags or flags[count] or matchFlag(name, flags) )
 		or blacklist and flags and not flags[count] and not matchFlag(name, flags) then
 			table.insert(references, reference)
 		else
 			text = removeString(text, reference)
 			if name then
 				for citation in string.gmatch(text, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?' .. escapeString(name) .. '[&quot;\']?[^/&gt;]*/%s*&gt;') do
 					text = removeString(text, citation)
 				end
 			end
 		end
 	end
 	return references, text
 end
 
 -- Get the lead section from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @return Wikitext of the lead section.
 local function getLead(text)
 	text = string.gsub('\n' .. text, '\n==.*', '')
 	text = mw.text.trim(text)
 	if not text then return throwError('lead-empty') end
 	return text
 end
 
 -- Get the requested sections from the given wikitext.
 -- @param text Required. Wikitext to parse.
 -- @param flags Range of sections to return, for example 2 or '1,3-5'. Omit to return all sections.
 -- @return Sequence of strings containing the wikitext of the requested sections.
 -- @return Original wikitext minus requested sections.
 local function getSections(text, flags)
 	local sections = {}
 	local flags, blacklist = parseFlags(flags)
 	local count = 0
 	local prefix, section, suffix
 	for title in string.gmatch('\n' .. text .. '\n==', '\n==+%s*([^=]+)%s*==+') do
 		count = count + 1
 		prefix, section, suffix = string.match('\n' .. text .. '\n==', '\n()==+%s*' .. escapeString(title) .. '%s*==+(.-)()\n==')
 		if not blacklist and ( not flags or flags[count] or matchFlag(title, flags) )
 		or blacklist and flags and not flags[count] and not matchFlag(title, flags) then
 			sections[title] = section
 		else
 			text = string.sub(text, 1, prefix) .. string.sub(text, suffix)
 			text = string.gsub(text, '\n?==$', '') -- remove the trailing \n==
 		end
 	end
 	return sections, text
 end
 
 -- Get the requested section or &lt;section&gt; tag from the given wikitext (including subsections).
 -- @param text Required. Wikitext to parse.
 -- @param section Required. Title of the section to get (in wikitext), for example 'History' or 'History of [[Athens]]'.
 -- @return Wikitext of the requested section.
 local function getSection(text, section)
 	section = mw.text.trim(section)
 	local escapedSection = escapeString(section)
 	-- First check if the section title matches a &lt;section&gt; tag
 	if string.find(text, '&lt;%s*[Ss]ection%s+begin%s*=%s*[&quot;\']?%s*' .. escapedSection .. '%s*[&quot;\']?%s*/&gt;') then -- avoid expensive search if possible
 		text = mw.text.trim((text
 			:gsub('&lt;%s*[Ss]ection%s+end=%s*[&quot;\']?%s*'.. escapedSection ..'%s*[&quot;\']?%s*/&gt;.-&lt;%s*[Ss]ection%s+begin%s*=%s*[&quot;\']?%s*' .. escapedSection .. '%s*[&quot;\']?%s*/&gt;', '') -- remove text between section tags
 			:gsub('^.-&lt;%s*[Ss]ection%s+begin%s*=%s*[&quot;\']?%s*' .. escapedSection .. '%s*[&quot;\']?%s*/&gt;', '') -- remove text before first section tag
 			:gsub('&lt;%s*[Ss]ection%s+end=%s*[&quot;\']?%s*'.. escapedSection ..'%s*[&quot;\']?%s*/&gt;.*', '') -- remove text after last section tag
 		))
 		if text == '' then return throwError('section-tag-empty', section) end
 		return text
 	end
 	local level, text = string.match('\n' .. text .. '\n', '\n(==+)%s*' .. escapedSection .. '%s*==.-\n(.*)')
 	if not text then return throwError('section-not-found', section) end
 	local nextSection = '\n==' .. string.rep('=?', #level - 2) .. '[^=].*'
 	text = string.gsub(text, nextSection, '') -- remove later sections with headings at this level or higher
 	text = mw.text.trim(text)
 	if text == '' then return throwError('section-empty', section) end
 	return text
 end
 
 -- Replace the first call to each reference defined outside of the text for the full reference, to prevent undefined references
 -- Then prefix the page title to the reference names to prevent conflicts
 -- that is, replace &lt;ref name=&quot;Foo&quot;&gt; for &lt;ref name=&quot;Title of the article Foo&quot;&gt;
 -- and also &lt;ref name=&quot;Foo&quot; /&gt; for &lt;ref name=&quot;Title of the article Foo&quot; /&gt;
 -- also remove reference groups: &lt;ref name=&quot;Foo&quot; group=&quot;Bar&quot;&gt; for &lt;ref name=&quot;Title of the article Foo&quot;&gt;
 -- and &lt;ref group=&quot;Bar&quot;&gt; for &lt;ref&gt;
 -- @todo The current regex may fail in cases with both kinds of quotes, like &lt;ref name=&quot;Darwin's book&quot;&gt;
 local function fixReferences(text, page, full)
 	if not full then full = getText(page) end
 	local refNames = {}
 	local refName
 	local refBody
 	local position = 1
 	while position &lt; mw.ustring.len(text) do
 		refName, position = mw.ustring.match(text, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?([^&quot;\'&gt;]+)[&quot;\']?[^&gt;]*/%s*&gt;()', position)
 		if refName then
 			refName = mw.text.trim(refName)
 			if not refNames[refName] then -- make sure we process each ref name only once
 				table.insert(refNames, refName)
 				refName = escapeString(refName)
 				refBody = mw.ustring.match(text, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?%s*' .. refName .. '%s*[&quot;\']?[^&gt;/]*&gt;.-&lt;%s*/%s*[Rr][Ee][Ff]%s*&gt;')
 				if not refBody then -- the ref body is not in the excerpt
 					refBody = mw.ustring.match(full, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?%s*' .. refName .. '%s*[&quot;\']?[^/&gt;]*&gt;.-&lt;%s*/%s*[Rr][Ee][Ff]%s*&gt;')
 					if refBody then -- the ref body was found elsewhere
 						text = mw.ustring.gsub(text, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?%s*' .. refName .. '%s*[&quot;\']?[^&gt;]*/?%s*&gt;', mw.ustring.gsub(refBody, '%%', '%%%%'), 1)
 					end
 				end
 			end
 		else
 			position = mw.ustring.len(text)
 		end
 	end
 	page = string.gsub(page, '&quot;', '') -- remove any quotation marks from the page title
 	text = mw.ustring.gsub(text, '&lt;%s*[Rr][Ee][Ff][^&gt;]*name%s*=%s*[&quot;\']?([^&quot;\'&gt;/]+)[&quot;\']?[^&gt;/]*(/?)%s*&gt;', '&lt;ref name=&quot;' .. page .. ' %1&quot;%2&gt;')
 	text = mw.ustring.gsub(text, '&lt;%s*[Rr][Ee][Ff]%s*group%s*=%s*[&quot;\']?[^&quot;\'&gt;/]+[&quot;\']%s*&gt;', '&lt;ref&gt;')
 	return text
 end
 
 -- Replace the bold title or synonym near the start of the page by a link to the page
 local function linkBold(text, page)
 	local lang = mw.language.getContentLanguage()
 	local position = mw.ustring.find(text, &quot;'''&quot; .. lang:ucfirst(page) .. &quot;'''&quot;, 1, true) -- look for &quot;'''Foo''' is...&quot; (uc) or &quot;A '''foo''' is...&quot; (lc)
 		or mw.ustring.find(text, &quot;'''&quot; .. lang:lcfirst(page) .. &quot;'''&quot;, 1, true) -- plain search: special characters in page represent themselves
 	if position then
 		local length = mw.ustring.len(page)
 		text = mw.ustring.sub(text, 1, position + 2) .. &quot;[[&quot; .. mw.ustring.sub(text, position + 3, position + length + 2) .. &quot;]]&quot; .. mw.ustring.sub(text, position + length + 3, -1) -- link it
 	else -- look for anything unlinked in bold, assumed to be a synonym of the title (e.g. a person's birth name)
 		text = mw.ustring.gsub(text, &quot;()'''(.-'*)'''&quot;, function(a, b)
 			if not mw.ustring.find(b, &quot;%[&quot;) and not mw.ustring.find(b, &quot;%{&quot;) then -- if not wikilinked or some weird template
 				return &quot;'''[[&quot; .. page .. &quot;|&quot; .. b .. &quot;]]'''&quot; -- replace '''Foo''' by '''[[page|Foo]]'''
 			else
 				return nil -- instruct gsub to make no change
 			end
 		end, 1) -- &quot;end&quot; here terminates the anonymous replacement function(a, b) passed to gsub
 	end
 	return text
 end
 
 -- Remove non-free files.
 -- @param text Required. Wikitext to clean.
 -- @return Clean wikitext.
 local function removeNonFreeFiles(text)
 	local fileNamespaces = getNamespaces('File')
 	local fileName
 	local fileDescription
 	local frame = mw.getCurrentFrame()
 	for file in string.gmatch(text, '%b[]') do
 		if matchAnyLink(file, fileNamespaces) then
 			fileName = 'File:' .. string.match(file, '%[%[[^:]-:([^]|]+)')
 			fileDescription, fileName = getText(fileName)
 			if fileName then
 				if not fileDescription or fileDescription == '' then
 					fileDescription = frame:preprocess('{{' .. fileName .. '}}') -- try Commons
 				end
 				if fileDescription and string.match(fileDescription, '[Nn]on%-free') then
 					text = removeString(text, file)
 				end
 			end
 		end
 	end
 	return text
 end
 
 -- Remove any self links
 local function removeSelfLinks(text)
 	local lang = mw.language.getContentLanguage()
 	local page = escapeString(mw.title.getCurrentTitle().prefixedText)
 	local ucpage = lang:ucfirst(page)
 	local lcpage = lang:lcfirst(page)
 	text = text
 		:gsub('%[%[(' .. ucpage .. ')%]%]', '%1')
 		:gsub('%[%[(' .. lcpage .. ')%]%]', '%1')
 		:gsub('%[%[' .. ucpage .. '|([^]]+)%]%]', '%1')
 		:gsub('%[%[' .. lcpage .. '|([^]]+)%]%]', '%1')
 	return text
 end
 
 -- Remove all wikilinks
 local function removeLinks(text)
 	text = text
 		:gsub('%[%[[^%]|]+|([^]]+)%]%]', '%1')
 		:gsub('%[%[([^]]+)%]%]', '%1')
 		:gsub('%[[^ ]+ ([^]]+)%]', '%1')
 		:gsub('%[([^]]+)%]', '%1')
 	return text
 end
 
 -- Remove HTML comments
 local function removeComments(text)
 	text = string.gsub(text, '&lt;!%-%-.-%-%-&gt;', '')
 	return text
 end
 
 -- Remove behavior switches, such as __NOTOC__
 local function removeBehaviorSwitches(text)
 	text = string.gsub(text, '__[A-Z]+__', '')
 	return text
 end
 
 -- Remove bold text
 local function removeBold(text)
 	text = string.gsub(text, &quot;'''&quot;, '')
 	return text
 end
 
 -- Main function for modules
 local function get(page, options)
 	if not options then options = {} end
 
 	-- Make sure the page exists
 	if not page then return throwError('no-page') end
 	page = mw.text.trim(page)
 	if page == '' then return throwError('no-page') end
 	local page, hash, section = string.match(page, '([^#]+)(#?)(.*)')
 	local text, temp = getText(page, options.noFollow)
 	if not temp then return throwError('invalid-title', page) end
 	page = temp
 	if not text then return throwError('page-not-found', page) end
 	local full = text -- save the full text for fixReferences below
 
 	-- Get the requested section
 	if truthy(section) then
 		text = getSection(text, section)
 	elseif truthy(hash) then
 		text = getLead(text)
 	end
 
 	-- Keep only the requested elements
 	local elements
 	if options.only then
 		if options.only == 'sections' then elements = getSections(text, options.sections) end
 		if options.only == 'lists' then elements = getLists(text, options.lists) end
 		if options.only == 'files' then elements = getFiles(text, options.files) end
 		if options.only == 'tables' then elements = getTables(text, options.tables) end
 		if options.only == 'templates' then elements = getTemplates(text, options.templates) end
 		if options.only == 'parameters' then elements = getParameters(text, options.parameters) end
 		if options.only == 'paragraphs' then elements = getParagraphs(text, options.paragraphs) end
 		if options.only == 'categories' then elements = getCategories(text, options.categories) end
 		if options.only == 'references' then elements = getReferences(text, options.references) end
 		text = ''
 		if elements then
 			for key, element in pairs(elements) do
 				text = text .. '\n' .. element .. '\n'
 			end
 		end
 	end
 
 	-- Filter the requested elements
 	if options.sections and options.only ~= 'sections' then elements, text = getSections(text, options.sections) end
 	if options.lists and options.only ~= 'lists' then elements, text = getLists(text, options.lists) end
 	if options.files and options.only ~= 'files' then elements, text = getFiles(text, options.files) end
 	if options.tables and options.only ~= 'tables' then elements, text = getTables(text, options.tables) end
 	if options.templates and options.only ~= 'templates' then elements, text = getTemplates(text, options.templates) end
 	if options.parameters and options.only ~= 'parameters' then elements, text = getParameters(text, options.parameters) end
 	if options.paragraphs and options.only ~= 'paragraphs' then elements, text = getParagraphs(text, options.paragraphs) end
 	if options.categories and options.only ~= 'categories' then elements, text = getCategories(text, options.categories) end
 	if options.references and options.only ~= 'references' then elements, text = getReferences(text, options.references) end
 
 	-- Misc options
 	if truthy(options.fixReferences) then text = fixReferences(text, page, full) end
 	if truthy(options.linkBold) and not truthy(section) then text = linkBold(text, page) end
 	if truthy(options.noBold) then text = removeBold(text) end
 	if truthy(options.noLinks) then text = removeLinks(text) end
 	if truthy(options.noSelfLinks) then text = removeSelfLinks(text) end
 	if truthy(options.noNonFreeFiles) then text = removeNonFreeFiles(text) end
 	if truthy(options.noBehaviorSwitches) then text = removeBehaviorSwitches(text) end
 	if truthy(options.noComments) then text = removeComments(text) end
 
 	-- Remove multiple newlines left over from removing elements
 	text = string.gsub(text, '\n\n\n+', '\n\n')
 	text = mw.text.trim(text)
 
 	return text
 end
 
 -- Main invocation function for templates
 local function main(frame)
 	local args = parseArgs(frame)
 	local page = args[1]
 	local ok, text = pcall(get, page, args)
 	if not ok then return getError(text) end
 	local raw = args['raw']
 	if raw then return text end
 	return frame:preprocess(text)
 end
 
 -- Entry points for templates
 function p.main(frame) return main(frame) end
 
 -- Entry points for modules
 function p.get(page, options) return get(page, options) end
 function p.getText(page, noFollow) return getText(page, noFollow) end
 function p.getLead(text) return getLead(text) end
 function p.getSection(text, section) return getSection(text, section) end
 function p.getSections(text, flags) return getSections(text, flags) end
 function p.getParagraphs(text, flags) return getParagraphs(text, flags) end
 function p.getParameters(text, flags) return getParameters(text, flags) end
 function p.getCategories(text, flags) return getCategories(text, flags) end
 function p.getReferences(text, flags) return getReferences(text, flags) end
 function p.getTemplates(text, flags) return getTemplates(text, flags) end
 function p.getTables(text, flags) return getTables(text, flags) end
 function p.getLists(text, flags) return getLists(text, flags) end
 function p.getFiles(text, flags) return getFiles(text, flags) end
 function p.getError(message, value) return getError(message, value) end
 
 -- Expose handy methods
 function p.truthy(value) return truthy(value) end
 function p.parseArgs(frame) return parseArgs(frame) end
 function p.matchAny(text, pre, list, post, init) return matchAny(text, pre, list, post, init) end
 function p.matchFlag(value, flags) return matchFlag(value, flags) end
 function p.getNamespaces(name) return getNamespaces(name) end
 function p.removeBold(text) return removeBold(text) end
 function p.removeLinks(text) return removeLinks(text) end
 function p.removeSelfLinks(text) return removeSelfLinks(text) end
 function p.removeNonFreeFiles(text) return removeNonFreeFiles(text) end
 function p.removeBehaviorSwitches(text) return removeBehaviorSwitches(text) end
 function p.removeComments(text) return removeComments(text) end