Aller au contenu

Module:Transcluder

Une page de Wikipédia, l'encyclopédie libre.
Ceci est une version archivée de cette page, en date du 14 avril 2022 à 08:02 et modifiée en dernier par Od1n (discuter | contributions) (les functions sont des first-class citizen en Lua, profitons-en). Elle peut contenir des erreurs, des inexactitudes ou des contenus vandalisés non présents dans la version actuelle.

 Documentation[voir] [modifier] [historique] [purger]
local p = {}

-- Helper function to test for truthy and falsy values
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 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

-- Helper function to escape a string for use in regexes
local function escapeString(str)
	return mw.ustring.gsub(str, '[%^%$%(%)%.%[%]%*%+%-%?%%]', '%%%0')
end

-- Helper function to remove a string from a text
local function removeString(text, str)
	local pattern = escapeString(str)
	if #pattern > 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 mw.ustring.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 wether 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 < 0 then
			value = value * -1
			blacklist = true
		end
		flags = { [value] = true }

	elseif type(value) == 'string' then
		if mw.ustring.sub(value, 1, 1) == '-' then
			blacklist = true
			value = mw.ustring.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 = mw.ustring.match(range, '^((%d+))$') end -- '1' to min=1 max=1
			if max then
				for p = min, max do flags[p] = 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()
	for flag in pairs(flags) do
		if value == tostring(flag)
		or lang:lcfirst(value) == flag
		or lang:ucfirst(value) == 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.site.namespaces[name].aliases
	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 <noinclude> tags
	text = mw.ustring.gsub(text, '<[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-</[Nn][Oo][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>', '') -- remove noinclude bits

	-- Keep <onlyinclude> tags
	if mw.ustring.find(text, '[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]') then -- avoid expensive search if possible
		text = mw.ustring.gsub(text, '</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>', '') -- remove text between onlyinclude sections
		text = mw.ustring.gsub(text, '^.-<[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>', '') -- remove text before first onlyinclude section
		text = mw.ustring.gsub(text, '</[Oo][Nn][Ll][Yy][Ii][Nn][Cc][Ll][Uu][Dd][Ee]>.*', '') -- 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 mw.ustring.gmatch(text, '%b[]') do
		if matchAny(file, '%[%[%s*', fileNamespaces, '%s*:.*%]%]') then
			name = mw.ustring.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 mw.ustring.gmatch('\n' .. text, '\n%b{}') do
		if mw.ustring.sub(t, 1, 3) == '\n{|' then
			id = mw.ustring.match(t, '\n{|[^\n]-id%s*=%s*["\']?([^"\'\n]+)["\']?[^\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 mw.ustring.gmatch(text, '{%b{}}') do
		if mw.ustring.sub(template, 1, 3) ~= '{{#' then -- skip parser functions like #if
			name = mw.text.trim( mw.ustring.match(template, '{{([^}|\n]+)') ) -- get the template name
			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
	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.
local function getParameters(text, flags)
	local parameters = {}
	local flags, blacklist = parseFlags(flags)
	local params, count, parts, key, value
	for template in mw.ustring.gmatch(text, '{%b{}}') do
		params = mw.ustring.match(template, '{{[^|}]-|(.+)}}')
		if params then
			count = 0
			-- Temporarily replace pipes in subtemplates, tables and links to avoid chaos
			for subtemplate in mw.ustring.gmatch(params, '%b{}') do
				params = mw.ustring.gsub(params, escapeString(subtemplate), mw.ustring.gsub(mw.ustring.gsub(subtemplate, '%%', '%%%'), '|', '@@@') )
			end
			for link in mw.ustring.gmatch(params, '%b[]') do
				params = mw.ustring.gsub(params, escapeString(link), mw.ustring.gsub(link, '|', '@@@') )
			end
			for parameter in mw.text.gsplit(params, '|') do
				parts = mw.text.split(parameter, '=')
				key = mw.text.trim(parts[1])
				value = table.concat(parts, '=', 2)
				if value == '' then
					value = key
					count = count + 1
					key = count
				else
					value = mw.text.trim(value)
				end
				value = mw.ustring.gsub(value, '@@@', '|')
				if not blacklist and ( not flags or matchFlag(key, flags) )
				or blacklist and flags and not matchFlag(key, flags) then
					parameters[key] = value
				else
					text = removeString(text, parameter)
				end
			end
		end
	end
	return parameters, text
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 mw.ustring.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.ustring.gsub(temp, '\n%b{}\n', '\n%0\n') -- add spacing between tables and block templates
	temp = mw.ustring.gsub(temp, '\n%b{}\n', '\n') -- remove tables and block templates
	temp = mw.ustring.gsub(temp, '\n==+[^=]+==+\n', '\n') -- remove section titles
	temp = mw.text.trim(temp)

	-- 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 mw.ustring.gmatch(text, '%b[]') do
		if matchAny(category, '%[%[%s*', categoryNamespaces, '%s*:.*%]%]') then
			name = mw.ustring.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 = {}
	if flags and not truthy(flags) then
		text = mw.ustring.gsub(text, '<%s*[Rr][Ee][Ff][^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>', '')
		text = mw.ustring.gsub(text, '<%s*[Rr][Ee][Ff][^>/]*/%s*>', '')
		return references, text
	end
	local flags, blacklist = parseFlags(flags)
	local name
	local count = 0
	for reference in mw.ustring.gmatch(text, '<%s*[Rr][Ee][Ff][^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>') do
		name = mw.ustring.match(reference, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?([^"\'>/]+)["\']?[^>]*%s*>')
		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 mw.ustring.gmatch(text, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?' .. escapeString(name) .. '["\']?[^/>]*/%s*>') 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 = mw.ustring.gsub('\n' .. text, '\n==.*', '')
	text = mw.text.trim(text)
	if not text then return throwError('lead-empty') end
	return text
end

-- Get the wikitext of the requested sections
-- @param text Required. Wikitext to parse.
-- @param flags Range of sections to return, for example 2 or '1,3-5'. Omit to return all references.
-- @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 mw.ustring.gmatch('\n' .. text .. '\n==', '\n==+%s*([^=]+)%s*==+\n') do
		count = count + 1
		prefix, section, suffix = mw.ustring.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 = mw.ustring.sub(text, 1, prefix) .. mw.ustring.sub(text, suffix)
			text = mw.ustring.gsub(text, '\n?==$', '') -- remove the trailing \n==
		end
	end
	return sections, text
end

-- Get the requested section 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 <section> tag
	if mw.ustring.find(text, '<%s*[Ss]ection%s+begin%s*=%s*["\']?%s*' .. escapedSection .. '%s*["\']?%s*/>') then -- avoid expensive search if possible
		text = mw.ustring.gsub(text, '<%s*[Ss]ection%s+end=%s*["\']?%s*'.. escapedSection ..'%s*["\']?%s*/>.-<%s*[Ss]ection%s+begin%s*=%s*["\']?%s*' .. escapedSection .. '%s*["\']?%s*/>', '') -- remove text between section tags
		text = mw.ustring.gsub(text, '^.-<%s*[Ss]ection%s+begin%s*=%s*["\']?%s*' .. escapedSection .. '%s*["\']?%s*/>', '') -- remove text before first section tag
		text = mw.ustring.gsub(text, '<%s*[Ss]ection%s+end=%s*["\']?%s*'.. escapedSection ..'%s*["\']?%s*/>.*', '') -- remove text after last section tag
		text = mw.text.trim(text)
		if text == '' then return throwError('section-tag-empty', section) end
		return text
	end
	local level, text = mw.ustring.match('\n' .. text .. '\n', '\n(==+)%s*' .. escapedSection .. '%s*==.-\n(.*)')
	if not text then return throwError('section-not-found', section) end
	local nextSection = '\n==' .. mw.ustring.rep('=?', #level - 2) .. '[^=].*'
	text = mw.ustring.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 <ref name="Foo"> for <ref name="Title of the article Foo">
-- and also <ref name="Foo" /> for <ref name="Title of the article Foo" />
-- also remove reference groups: <ref name="Foo" group="Bar"> for <ref name="Title of the article Foo">
-- and <ref group="Bar"> for <ref>
-- @todo The current regex may fail in cases with both kinds of quotes, like <ref name="Darwin's book">
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 < mw.ustring.len(text) do
		refName, position = mw.ustring.match(text, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?([^"\'>]+)["\']?[^>]*/%s*>()', 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, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?%s*' .. refName .. '%s*["\']?[^>/]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>')
				if not refBody then -- the ref body is not in the excerpt
					refBody = mw.ustring.match(full, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?%s*' .. refName .. '%s*["\']?[^/>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>')
					if refBody then -- the ref body was found elsewhere
						text = mw.ustring.gsub(text, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?%s*' .. refName .. '%s*["\']?[^>]*/?%s*>', mw.ustring.gsub(refBody, '%%', '%%%%'), 1)
					end
				end
			end
		else
			position = mw.ustring.len(text)
		end
	end
	text = mw.ustring.gsub(text, '<%s*[Rr][Ee][Ff][^>]*name%s*=%s*["\']?([^"\'>/]+)["\']?[^>/]*(/?)%s*>', '<ref name="' .. page .. ' %1"%2>')
	text = mw.ustring.gsub(text, '<%s*[Rr][Ee][Ff]%s*group%s*=%s*["\']?[^"\'>/]+["\']%s*>', '<ref>')
	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, "'''" .. lang:ucfirst(page) .. "'''", 1, true) -- look for "'''Foo''' is..." (uc) or "A '''foo''' is..." (lc)
		or mw.ustring.find(text, "'''" .. lang:lcfirst(page) .. "'''", 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) .. "[[" .. mw.ustring.sub(text, position + 3, position + length + 2) .. "]]" .. 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, "()'''(.-'*)'''", function(a, b)
			if not mw.ustring.find(b, "%[") and not mw.ustring.find(b, "%{") then -- if not wikilinked or some weird template
				return "'''[[" .. page .. "|" .. b .. "]]'''" -- replace '''Foo''' by '''[[page|Foo]]'''
			else
				return nil -- instruct gsub to make no change
			end
		 end, 1) -- "end" 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 mw.ustring.gmatch(text, '%b[]') do
		if matchAny(file, '%[%[%s*', fileNamespaces, '%s*:.*%]%]') then
			fileName = 'File:' .. mw.ustring.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 mw.ustring.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 )
	text = mw.ustring.gsub(text, '%[%[(' .. lang:ucfirst(page) .. ')%]%]', '%1')
	text = mw.ustring.gsub(text, '%[%[(' .. lang:lcfirst(page) .. ')%]%]', '%1')
	text = mw.ustring.gsub(text, '%[%[' .. lang:ucfirst(page) .. '|([^]]+)%]%]', '%1')
	text = mw.ustring.gsub(text, '%[%[' .. lang:lcfirst(page) .. '|([^]]+)%]%]', '%1')
	return text
end

-- Remove all wikilinks
local function removeLinks(text)
	text = mw.ustring.gsub(text, '%[%[[^|]+|([^]]+)%]%]', '%1')
	text = mw.ustring.gsub(text, '%[%[([^]]+)%]%]', '%1')
	return text
end

-- Remove HTML comments
local function removeComments(text)
	text = mw.ustring.gsub(text, '<!%-%-.-%-%->', '')
	return text
end

-- Remove behavior switches, such as __NOTOC__
local function removeBehaviorSwitches(text)
	text = mw.ustring.gsub(text, '__[A-Z]+__', '')
	return text
end

-- Remove bold text
local function removeBold(text)
	text = mw.ustring.gsub(text, "'''", '')
	return text
end

-- Main function for modules
local function get(page, options)
	if not options then options = {} end

	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 = mw.ustring.match(page, '([^#]+)(#?)([^#]*)')
	local text, page = getText(page, options.noFollow)
	if not page then return throwError('no-page') end
	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) 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 = mw.ustring.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
	return frame:preprocess(text)
end

-- Entry points for templates
p.main = main

-- Entry points for modules
p.get = get
p.getText = getText
p.getLead = getLead
p.getSection = getSection
p.getSections = getSections
p.getParagraphs = getParagraphs
p.getParameters = getParameters
p.getCategories = getCategories
p.getReferences = getReferences
p.getTemplates = getTemplates
p.getTables = getTables
p.getLists = getLists
p.getFiles = getFiles
p.getError = getError

-- Expose handy methods
p.truthy = truthy
p.parseArgs = parseArgs
p.matchAny = matchAny
p.matchFlag = matchFlag
p.getNamespaces = getNamespaces
p.removeBold = removeBold
p.removeLinks = removeLinks
p.removeSelfLinks = removeSelfLinks
p.removeNonFreeFiles = removeNonFreeFiles
p.removeBehaviorSwitches = removeBehaviorSwitches
p.removeComments = removeComments

return p