Module:utilities: Difference between revisions

From Linguifex
Jump to navigation Jump to search
No edit summary
No edit summary
 
(9 intermediate revisions by the same user not shown)
Line 1: Line 1:
local export = {}
local export = {}


local data = mw.loadData("Module:utilities/data")
local headword_data_module = "Module:headword/data"
local notneeded = data.notneeded
local languages_module = "Module:languages"
local neededhassubpage = data.neededhassubpage
local links_module = "Module:links"
local load_module = "Module:load"
local pages_module = "Module:pages"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local utilities_data_module = "Module:utilities/data"


-- A helper function to escape magic characters in a string
local mw = mw
-- Magic characters: ^$()%.[]*+-?
 
function export.pattern_escape(text)
local anchor_encode = mw.uri.anchorEncode
if type(text) == "table" then
local concat = table.concat
text = text.args[1]
local format_categories -- Defined below.
local ipairs = ipairs
local require = require
local type = type
local unstrip = mw.text.unstrip
 
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
end
text = mw.ustring.gsub(text, "([%^$()%%.%[%]*+%-?])", "%%%1")
return text
end
function export.plain_gsub(text, pattern, replacement)
local invoked = false
if type(text) == "table" then
local function get_script(...)
invoked = true
get_script = require(scripts_module).getByCode
return get_script(...)
if text.args then
local frame = text
local params = {
[1] = {},
[2] = {},
[3] = { allow_empty = true },
}
local args = require("Module:parameters").process(frame.args, params)
text = args[1]
pattern = args[2]
replacement = args[3]
else
error("If the first argument to plain_gsub is a table, it should be a frame object.")
end
else
if not ( type(pattern) == "string" or type(pattern) == "number" ) then
error("The second argument to plain_gsub should be a string or a number.")
end
if not ( type(replacement) == "string" or type(replacement) == "number" ) then
error("The third argument to plain_gsub should be a string or a number.")
end
end
end
pattern = export.pattern_escape(pattern)
local function is_content_page(...)
is_content_page = require(pages_module).is_content_page
return is_content_page(...)
end
if invoked then
local function load_data(...)
text = mw.ustring.gsub(text, pattern, replacement)
load_data = require(load_module).load_data
return text
return load_data(...)
else
return mw.ustring.gsub(text, pattern, replacement)
end
end
end
--[[
Format the categories with the appropriate sort key. CATEGORIES is a list of
categories.
-- LANG is an object encapsulating a language; if nil, the object for
  language code 'und' (undetermined) will be used.
-- SORT_KEY is placed in the category invocation, and indicates how the
  page will sort in the respective category. Normally this should be nil,
  and a default sort key based on the subpage name (the part after the
  colon) will be used.
-- SORT_BASE lets you override the default sort key used when SORT_KEY is
  nil. Normally, this should be nil, and a language-specific default sort
  key is computed from the subpage name (e.g. for Russian this converts
  Cyrillic ё to a string consisting of Cyrillic е followed by U+10FFFF,
  so that effectively ё sorts after е instead of the default Wikimedia
  sort, which (I think) is based on Unicode sort order and puts ё after я,
  the last letter of the Cyrillic alphabet.
-- FORCE_OUTPUT forces normal output in all namespaces. Normally, nothing
  is output if the page isn't in the main, Appendix:, Reconstruction: or
  Citations: namespaces.
]]
function export.format_categories(categories, lang, sort_key, sort_base, force_output, sc)
local NAMESPACE = mw.title.getCurrentTitle().nsText
if type(lang) == "table" and not lang.getCode then
local function remove_links(...)
error("The second argument to format_categories should be a language object.")
remove_links = require(links_module).remove_links
return remove_links(...)
end
end
if force_output or data.allowedNamespaces[NAMESPACE] then
local function tag_text(...)
local PAGENAME = mw.title.getCurrentTitle().text
tag_text = require(script_utilities_module).tag_text
local SUBPAGENAME = mw.title.getCurrentTitle().subpageText
return tag_text(...)
if not lang then
lang = require("Module:languages").getByCode("und")
end
-- Generate a default sort key
sort_base = lang:makeSortKey(sort_base or SUBPAGENAME, sc)
if sort_key and sort_key ~= "" then
-- Gather some statistics regarding sort keys
if mw.ustring.upper(sort_key) == sort_base then
table.insert(categories, "Sort key tracking/redundant")
end
else
sort_key = sort_base
end
-- If the sortkey is empty, remove it.
-- Leave the sortkey if it is equal to PAGENAME, because it still
-- might be different from DEFAULTSORT and therefore have an effect; see
-- [[Wiktionary:Grease pit/2020/April#Module:utilities#format categories]].
if sort_key == "" then
sort_key = nil
end
local out_categories = {}
for key, cat in ipairs(categories) do
out_categories[key] = "[[Category:" .. table.concat(cat) .. (sort_key and "|" .. sort_key or "") .. "]]"
end
return table.concat(out_categories, "")
else
return ""
end
end
end
-- Used by {{categorize}}
function export.template_categorize(frame)
local NAMESPACE = mw.title.getCurrentTitle().nsText
local format = frame.args["format"]
local args = frame:getParent().args
local langcode = args[1]; if langcode == "" then langcode = nil end
local function trim(...)
local sort_key = args["sort"]; if sort_key == "" then sort_key = nil end
trim = require(string_utilities_module).trim
local categories = {}
return trim(...)
end
if not langcode then
local function uupper(...)
if NAMESPACE == "Template" then return "" end
uupper = require(string_utilities_module).upper
error("Language code has not been specified. Please pass parameter 1 to the template.")
return uupper(...)
end
 
--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
local catfix_scripts
local function get_catfix_scripts()
catfix_scripts, get_catfix_scripts = load_data(utilities_data_module).catfix_scripts, nil
return catfix_scripts
end
end
local lang = require("Module:languages").getByCode(langcode)
local current_title
local function get_current_title()
current_title, get_current_title = mw.title.getCurrentTitle(), nil
return current_title
end
if not lang then
local defaultsort
if NAMESPACE == "Template" then return "" end
local function get_defaultsort()
error("The language code \"" .. langcode .. "\" is not valid.")
defaultsort, get_defaultsort = load_data(headword_data_module).page.pagename_defaultsort, nil
return defaultsort
end
end
local prefix = ""
local pagename
local function get_pagename()
pagename, get_pagename = load_data(headword_data_module).page.encoded_pagename, nil
return pagename
end
 
local und
local function get_und()
und, get_und = require(languages_module).getByCode("und"), nil
return und
end
 
 
do
local function handle_url(capture)
return capture:match("https?://[^%s%]]+%s([^%]]+)") or ""
end
if format == "pos" then
--[==[
prefix = lang:getCanonicalName() .. " "
A helper function to strip wiki markup, giving the plaintext of what is displayed on the page.]==]
elseif format == "topic" then
function export.get_plaintext(text)
prefix = lang:getCode() .. ":"
text = text
:gsub("%[%[", "\1")
:gsub("%]%]", "\2")
 
-- Remove strip markers and HTML tags.
text = unstrip(text):gsub("<[^<>\1\2]+>", "")
 
-- Parse internal links for the display text, and remove categories.
text = remove_links(text)
 
-- Remove files.
text = text:gsub("\1[Ff][Ii][Ll][Ee]:[^\1\2]+\2", "")
:gsub("\1[Ii][Mm][Aa][Gg][Ee]:[^\1\2]+\2", "")
 
-- Parse external links for the display text.
text = text:gsub("%[(https?://[^%[%]]+)%]", handle_url)
-- Any remaining square brackets aren't involved in links, but must be escaped to avoid creating new links.
:gsub("\1", "&#91;&#91;")
:gsub("\2", "&#93;&#93;")
:gsub("%[", "&#91;")
:gsub("]", "&#93;")
-- Strip bold, italics and soft hyphens.
:gsub("('*)'''(.-'*)'''", "%1%2")
:gsub("('*)''(.-'*)''", "%1%2")
:gsub("­", "")
-- Get any HTML entities and trim.
-- Note: don't decode URL percent encoding, as it shouldn't be used in display text and may cause problems if % is used.
return trim(decode_entities(text))
end
end
end
local i = 2
 
local cat = args[i]
do
local function generate_sortkey(lang, sc, sort_key, sort_base)
while cat do
-- If the sort key is "-", treat the language as undetermined (the default). This is desirable when categorising (e.g.) translation requests, as the pages to be categorised are always in English/Translingual.
if cat ~= "" then
if sort_key == "-" then
table.insert(categories, prefix .. cat)
lang, sort_key = und or get_und(), nil
elseif not lang then
lang = und or get_und()
end
-- Generate the automatic sort key.
local auto = lang:makeSortKey(sort_base or pagename or get_pagename(), sc)
-- Use the page defaultsort if necessary.
if not auto or auto == "" then
auto = defaultsort or get_defaultsort()
end
-- If not sort key specified, use the automatic one.
if not sort_key or sort_key == "" then
return auto
-- Otherwise, if the language is not "und", categorize the manual sort key as either redundant or non-redundant.
-- FIXME: we should do this for "und" as well, but "Undetermined terms..." does not make sense for translations etc.
elseif lang:getCode() ~= "und" then
return sort_key, "[[Category:" .. lang:getFullName() .. " terms with " .. (
uupper(sort_key) == auto and "redundant" or
"non-redundant non-automated"
) .. " sortkeys|" .. sort_key .. "]]"
end
return sort_key
end
 
--[==[
Format the categories with the appropriate sort key.
* `cat` can take two forms:
  ** A string (the full category, minus the {"Category:"} prefix);
  ** A list of categories. Each category in the list can be either:
  *** A string in the same format as above;
  *** An object with the fields:
  **** `cat`: a string in the same format as above (required);
  **** `lang`: an optional language object to override the overall `lang`;
  **** `sc`: an optional script object to override the overall `sc`.
  **** `sort_key`: an optional sort key to override the overall `sort_key`;
  **** `sort_base`: an optional sort base to override the overall `sort_base`;
* `lang` is an object encapsulating a language; if {nil}, the object for language code {"und"} (undetermined) will
  be used. `lang` is used when computing the sort key (either from the subpage name or sort base).
* `sort_key` is placed in the category invocation, and indicates how the page will sort in the respective category.
  Normally '''do not use this'''. Instead, leave it {nil}, and if you need to a control the sort order, use
  {sort_base}, so that language-specific normalization is applied on top of the specified sort base. If neither
  {sort_key} nor {sort_base} is specified, the default is to apply language-specific normalization to the subpage
  name; see below.
* `sort_base` lets you override the default sort key while still maintaining appropriate language-specific
  normalization. If {nil} is specified, this defaults to the subpage name, which is the portion of the full pagename
  after subtracting the namespace prefix (and, in certain namespaces such as {User:}, but notably not in the
  mainspace, after subtracting anything up through the final slash). The actual sort key is derived from the sort
  base approximately by lowercasing, applying language-specific normalization and then uppercasing; note that the
  same process is applied in deriving the sort key when no sort base is specified. For example, for French, Spanish,
  etc. the normalization process maps accented letters to their unaccented equivalents, so that e.g. in French,
  {{m|fr|ça}} sorts after {{m|fr|ca}} (instead of after the default Wikimedia sort order, which is approximately
  based on Unicode sort order and places ç after z) and {{m|fr|côté}} sorts after {{m|fr|coté}} (instead of between
  c and d). Similarly, in Russian the normalization process converts Cyrillic ё to a string consisting of Cyrillic е
  followed by U+10FFFF, so that effectively ё sorts after е instead of the default Wikimedia sort, which (I think)
  puts ё after я, the last letter of the Cyrillic alphabet.
* `force_output` forces normal output in all namespaces. Normally, nothing is output if the page isn't in the main,
  Appendix:, Thesaurus:, Reconstruction: or Citations: namespaces.
* `sc` is a script object; if nil, the default will be derived from the sort base (or its default value, the
  subpage name) by calling {lang:findBestScript()}. The value of `sc` is used during the sort base normalization
  process; for example, languages with multiple scripts will often have script-specific normalization processes.]==]
function export.format_categories(cat, lang, sort_key, sort_base, force_output, sc)
if not (
force_output or
is_content_page(current_title or get_current_title()) or
current_title.prefixedText == "Wiktionary:Sandbox"
) then
return ""
elseif type(cat) == "string" then
local this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
return "[[Category:" .. cat .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
local ret, i, n, default = {}, 0, 0
-- Iterate over all categories in `cat`.
while true do
i = i + 1
local category = cat[i]
if category == nil then
return concat(ret)
end
local this_sort_key, extra
-- If the category type is a table, use any custom options in it.
if type(category) == "table" then
category, this_sort_key, extra = category.cat, generate_sortkey(
category.lang or lang,
category.sc or sc,
category.sort_key or sort_key,
category.sort_base or sort_base
)
-- If `default` has already been determined, use it.
elseif default then
this_sort_key = default
-- Otherwise, calculate `default` and use it.
else
this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
default = this_sort_key
end
n = n + 1
ret[n] = "[[Category:" .. category .. "|" .. this_sort_key .. "]]" .. (extra or "")
end
end
i = i + 1
cat = args[i]
end
end
format_categories = export.format_categories
return export.format_categories(categories, lang, sort_key)
end
end


--[==[
Add a "catfix", which is used on language-specific category pages to add language attributes and often script
classes to all entry names. The addition of language attributes and script classes makes the entry names display
better (using the language- or script-specific styles specified in [[MediaWiki:Common.css]]), which is particularly
important for non-English languages that do not have consistent font support in browsers.
Language attributes are added for all languages, but script classes are only added for languages with one script
listed in their data file, or for languages that have a default script listed in the {catfix_script} list in
[[Module:utilities/data]]. Some languages clearly have a default script, but still have other scripts listed in
their data file. If those other scripts are not simply scripts like {{cd|Brai}} (Braille), their default script
needs to be specified. Others do not have a default script.
* Serbo-Croatian is regularly written in both the Latin and Cyrillic scripts. Because it uses two scripts,
  Serbo-Croatian cannot have a script class applied to entries in its category pages, as only one script class
  can be specified at a time.
* German is usually written in the Latin script ({{cd|Latn}}), but Fraktur ({{cd|Latf}}) is also listed in
  its data file. So German needs an entry in the {catfix_script} list, so that the {{cd|Latn}} (Latin) script
  class will be applied to entries in its category pages.
To find the scripts listed for a language, go to [[Module:languages]] and use the search box to find the data file
for the language. To find out what a script code means, search the script code in [[Module:scripts/data]].]==]
function export.catfix(lang, sc)
function export.catfix(lang, sc)
if not lang then
if not lang or not lang.getCanonicalName then
require("Module:debug").track("catfix/no lang")
error('The first argument to the function "catfix" should be a language object from [[Module:languages]] or [[Module:etymology languages]].')
return nil
elseif type(lang) ~= "table" then
require("Module:debug").track("catfix/lang not table")
return nil
end
end
local canonicalName = lang:getCanonicalName() or error('The first argument to the function "catfix" should be a language object from Module:languages.')
if sc and not sc.getCode then
if sc and not sc.getCode then
error('The second argument to the function "catfix" should be a script object from Module:scripts.')
error('The second argument to the function "catfix" should be a script object from [[Module:scripts]].')
end
end
 
-- To add script classes to links on pages created by category boilerplate templates.
-- To add script classes to links on pages created by category boilerplate templates.
if not sc then
if not sc then
sc = data.catfix_scripts[lang:getCode()]
local code = (catfix_scripts or get_catfix_scripts())[lang:getCode()] or catfix_scripts[lang:getFullCode()]
if sc then
if code then
sc = require("Module:scripts").getByCode(sc)
sc = get_script(code)
end
end
end
end
return "<span id=\"catfix\" style=\"display:none;\" class=\"CATFIX-" .. mw.uri.anchorEncode(canonicalName) .. "\">" ..
require("Module:script utilities").tag_text("&nbsp;", lang, sc, nil) ..
"</span>"
end


function export.catfix_template(frame)
-- If the language only has a single valid candidate script, apply it as the default.
local params = {
if not sc then
[1] = {},
local scripts = lang:getScripts()
[2] = { alias_of = "sc" },
if #scripts == 1 then
["sc"] = {},
sc = scripts[1]
}
else
-- Iterate over scripts. If there is only one when ignoring
local args = require("Module:parameters").process(frame:getParent().args, params)
-- scripts like Brai, then pick that one.
for _, script in ipairs(scripts) do
local lang = require("Module:languages").getByCode(args[1]) or require("Module:languages").err(args[1], 1)
if script:getCode() ~= "Brai" then
if sc then
local sc = args.sc
-- Multiple candidates - fail.
if sc then
sc = nil
sc = require("Module:scripts").getByCode(sc) or error('The script code "' .. sc .. '", provided in the second parameter, is not valid.')
break
else
sc = script
end
end
end
end
end
end
return export.catfix(lang, sc)
-- Hack: using a <ul> tag prevents the parser from automatically generating a <p> tag around the catfix element.
return "<ul class=\"catfix\" data-anchor=\"" ..
anchor_encode(lang:getFullName()) .. "\">" ..
tag_text("", lang, sc) .. "</ul>"
end
end


-- Not exporting because it is not used yet.
--[==[
local function getDateTense(frame)
Given a type (as a string) and an arbitrary number of entities, checks whether all of those entities are language,
local name_num_mapping = {["January"] = 1, ["February"] = 2, ["March"] = 3, ["April"] = 4, ["May"] = 5, ["June"] = 6,
family, script, writing system or Wikimedia language objects. Useful for error handling in functions that require
["July"] = 7, ["August"] = 8, ["September"] = 9, ["October"] = 10, ["November"] = 11, ["December"] = 12,  
one of these kinds of object.
[1] = 1, [2] = 2, [3] = 3, [4] = 4, [5] = 5, [6] = 6, [7] = 7, [8] = 8, [9] = 9, [10] = 10, [11] = 11, [12] = 12}
local month = name_num_mapping[frame.args[2]]
local date = os.time({year = frame.args[1], day = frame.args[3], month = month})
local today = os.time() -- 12 AM/PM
local diff = os.difftime(date, today)
local daylength = 24 * 3600
if diff < -daylength / 2 then return "past"
else
if diff > daylength / 2  then return "future"
else return "present" end
end
end


function export.make_id(lang, str)
If `noErr` is set, the function returns false instead of throwing an error, which allows customised error handling to
--[[ If called with invoke, first argument is a frame object.
be done in the calling function.]==]
If called by a module, first argument is a language object. ]]
function export.check_object(typ, noErr, ...)
local invoked = false
if ... == nil then
if noErr then
if type(lang) == "table" then
return false
if lang.args then
invoked = true
local frame = lang
local params = {
[1] = {},
[2] = {},
}
local args = require("Module:parameters").process(frame:getParent().args, params)
local langCode = args[1]
str = args[2]
local m_languages = require("Module:languages")
lang = m_languages.getByCode(langCode) or m_languages.err(langCode, 1)
elseif not lang.getCanonicalName then
error("The first argument to make_id should be a language object.")
end
end
error("Must provide at least one object to check.", 2)
end
end
 
for _, obj in ipairs{...} do
if not ( type(str) == "string" or type(str) == "number" ) then
if type(obj) ~= "table" or type(obj.hasType) ~= "function" then
error("The second argument to make_id should be a string or a number.")
if noErr then
end
return false
end
local canonicalName = lang:getCanonicalName()
error("Function expected a " .. typ .. " object, but received a " .. type(obj) .. " instead.", 2)
elseif not (typ == "object" or obj:hasType(typ)) then
str = mw.uri.encode(str, "WIKI")
for _, wrong_type in ipairs{"family", "language", "script", "Wikimedia language", "writing system"} do
if obj:hasType(wrong_type) then
local id = canonicalName .. "-" .. str
if noErr then
return false
if invoked then
end
return '<li class="senseid" id="' .. id .. '">'
error("Function expected a " .. typ .. " object, but received a " .. wrong_type .. " object instead.", 2)
else
end
return id
end
if noErr then
return false
end
error("Function expected a " .. typ .. " object, but received another type of object instead.", 2)
end
end
end
return true
end
end


return export
return export

Latest revision as of 13:40, 25 January 2025



local export = {}

local headword_data_module = "Module:headword/data"
local languages_module = "Module:languages"
local links_module = "Module:links"
local load_module = "Module:load"
local pages_module = "Module:pages"
local script_utilities_module = "Module:script utilities"
local scripts_module = "Module:scripts"
local string_utilities_module = "Module:string utilities"
local utilities_data_module = "Module:utilities/data"

local mw = mw

local anchor_encode = mw.uri.anchorEncode
local concat = table.concat
local format_categories -- Defined below.
local ipairs = ipairs
local require = require
local type = type
local unstrip = mw.text.unstrip

--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
	local function decode_entities(...)
		decode_entities = require(string_utilities_module).decode_entities
		return decode_entities(...)
	end
	
	local function get_script(...)
		get_script = require(scripts_module).getByCode
		return get_script(...)
	end
	
	local function is_content_page(...)
		is_content_page = require(pages_module).is_content_page
		return is_content_page(...)
	end
	
	local function load_data(...)
		load_data = require(load_module).load_data
		return load_data(...)
	end
	
	local function remove_links(...)
		remove_links = require(links_module).remove_links
		return remove_links(...)
	end
	
	local function tag_text(...)
		tag_text = require(script_utilities_module).tag_text
		return tag_text(...)
	end
	
	local function trim(...)
		trim = require(string_utilities_module).trim
		return trim(...)
	end
	
	local function uupper(...)
		uupper = require(string_utilities_module).upper
		return uupper(...)
	end

--[==[
Loaders for objects, which load data (or some other object) into some variable, which can then be accessed as "foo or get_foo()", where the function get_foo sets the object to "foo" and then returns it. This ensures they are only loaded when needed, and avoids the need to check for the existence of the object each time, since once "foo" has been set, "get_foo" will not be called again.]==]
	local catfix_scripts
	local function get_catfix_scripts()
		catfix_scripts, get_catfix_scripts = load_data(utilities_data_module).catfix_scripts, nil
		return catfix_scripts
	end
	
	local current_title
	local function get_current_title()
		current_title, get_current_title = mw.title.getCurrentTitle(), nil
		return current_title
	end
	
	local defaultsort
	local function get_defaultsort()
		defaultsort, get_defaultsort = load_data(headword_data_module).page.pagename_defaultsort, nil
		return defaultsort
	end
	
	local pagename
	local function get_pagename()
		pagename, get_pagename = load_data(headword_data_module).page.encoded_pagename, nil
		return pagename
	end

	local und
	local function get_und()
		und, get_und = require(languages_module).getByCode("und"), nil
		return und
	end


do
	local function handle_url(capture)
		return capture:match("https?://[^%s%]]+%s([^%]]+)") or ""
	end
	
	--[==[
	A helper function to strip wiki markup, giving the plaintext of what is displayed on the page.]==]
	function export.get_plaintext(text)
		text = text
			:gsub("%[%[", "\1")
			:gsub("%]%]", "\2")

		-- Remove strip markers and HTML tags.
		text = unstrip(text):gsub("<[^<>\1\2]+>", "")

		-- Parse internal links for the display text, and remove categories.
		text = remove_links(text)

		-- Remove files.
		text = text:gsub("\1[Ff][Ii][Ll][Ee]:[^\1\2]+\2", "")
			:gsub("\1[Ii][Mm][Aa][Gg][Ee]:[^\1\2]+\2", "")

		-- Parse external links for the display text.
		text = text:gsub("%[(https?://[^%[%]]+)%]", handle_url)
			-- Any remaining square brackets aren't involved in links, but must be escaped to avoid creating new links.
			:gsub("\1", "&#91;&#91;")
			:gsub("\2", "&#93;&#93;")
			:gsub("%[", "&#91;")
			:gsub("]", "&#93;")
			-- Strip bold, italics and soft hyphens.
			:gsub("('*)'''(.-'*)'''", "%1%2")
			:gsub("('*)''(.-'*)''", "%1%2")
			:gsub("­", "")
		
		-- Get any HTML entities and trim.
		-- Note: don't decode URL percent encoding, as it shouldn't be used in display text and may cause problems if % is used.
		return trim(decode_entities(text))
	end
end

do
	local function generate_sortkey(lang, sc, sort_key, sort_base)
		-- If the sort key is "-", treat the language as undetermined (the default). This is desirable when categorising (e.g.) translation requests, as the pages to be categorised are always in English/Translingual.
		if sort_key == "-" then
			lang, sort_key = und or get_und(), nil
		elseif not lang then
			lang = und or get_und()
		end
		-- Generate the automatic sort key.
		local auto = lang:makeSortKey(sort_base or pagename or get_pagename(), sc)
		-- Use the page defaultsort if necessary.
		if not auto or auto == "" then
			auto = defaultsort or get_defaultsort()
		end
		-- If not sort key specified, use the automatic one.
		if not sort_key or sort_key == "" then
			return auto
		-- Otherwise, if the language is not "und", categorize the manual sort key as either redundant or non-redundant.
		-- FIXME: we should do this for "und" as well, but "Undetermined terms..." does not make sense for translations etc.
		elseif lang:getCode() ~= "und" then
			return sort_key, "[[Category:" .. lang:getFullName() .. " terms with " .. (
				uupper(sort_key) == auto and "redundant" or
				"non-redundant non-automated"
			) .. " sortkeys|" .. sort_key .. "]]"
		end
		return sort_key
	end

	--[==[
	Format the categories with the appropriate sort key.
	* `cat` can take two forms:
	  ** A string (the full category, minus the {"Category:"} prefix);
	  ** A list of categories. Each category in the list can be either:
	  *** A string in the same format as above;
	  *** An object with the fields:
	  **** `cat`: a string in the same format as above (required);
	  **** `lang`: an optional language object to override the overall `lang`;
	  **** `sc`: an optional script object to override the overall `sc`.
	  **** `sort_key`: an optional sort key to override the overall `sort_key`;
	  **** `sort_base`: an optional sort base to override the overall `sort_base`;
	* `lang` is an object encapsulating a language; if {nil}, the object for language code {"und"} (undetermined) will
	  be used. `lang` is used when computing the sort key (either from the subpage name or sort base).
	* `sort_key` is placed in the category invocation, and indicates how the page will sort in the respective category.
	  Normally '''do not use this'''. Instead, leave it {nil}, and if you need to a control the sort order, use
	  {sort_base}, so that language-specific normalization is applied on top of the specified sort base. If neither
	  {sort_key} nor {sort_base} is specified, the default is to apply language-specific normalization to the subpage
	  name; see below.
	* `sort_base` lets you override the default sort key while still maintaining appropriate language-specific
	  normalization. If {nil} is specified, this defaults to the subpage name, which is the portion of the full pagename
	  after subtracting the namespace prefix (and, in certain namespaces such as {User:}, but notably not in the
	  mainspace, after subtracting anything up through the final slash). The actual sort key is derived from the sort
	  base approximately by lowercasing, applying language-specific normalization and then uppercasing; note that the
	  same process is applied in deriving the sort key when no sort base is specified. For example, for French, Spanish,
	  etc. the normalization process maps accented letters to their unaccented equivalents, so that e.g. in French,
	  {{m|fr|ça}} sorts after {{m|fr|ca}} (instead of after the default Wikimedia sort order, which is approximately
	  based on Unicode sort order and places ç after z) and {{m|fr|côté}} sorts after {{m|fr|coté}} (instead of between
	  c and d). Similarly, in Russian the normalization process converts Cyrillic ё to a string consisting of Cyrillic е
	  followed by U+10FFFF, so that effectively ё sorts after е instead of the default Wikimedia sort, which (I think)
	  puts ё after я, the last letter of the Cyrillic alphabet.
	* `force_output` forces normal output in all namespaces. Normally, nothing is output if the page isn't in the main,
	  Appendix:, Thesaurus:, Reconstruction: or Citations: namespaces.
	* `sc` is a script object; if nil, the default will be derived from the sort base (or its default value, the
	  subpage name) by calling {lang:findBestScript()}. The value of `sc` is used during the sort base normalization
	  process; for example, languages with multiple scripts will often have script-specific normalization processes.]==]
	function export.format_categories(cat, lang, sort_key, sort_base, force_output, sc)
		if not (
			force_output or
			is_content_page(current_title or get_current_title()) or
			current_title.prefixedText == "Wiktionary:Sandbox"
		) then
			return ""
		elseif type(cat) == "string" then
			local this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
			return "[[Category:" .. cat .. "|" .. this_sort_key .. "]]" .. (extra or "")
		end
		local ret, i, n, default = {}, 0, 0
		-- Iterate over all categories in `cat`.
		while true do
			i = i + 1
			local category = cat[i]
			if category == nil then
				return concat(ret)
			end
			local this_sort_key, extra
			-- If the category type is a table, use any custom options in it.
			if type(category) == "table" then
				category, this_sort_key, extra = category.cat, generate_sortkey(
					category.lang or lang,
					category.sc or sc,
					category.sort_key or sort_key,
					category.sort_base or sort_base
				)
			-- If `default` has already been determined, use it.
			elseif default then
				this_sort_key = default
			-- Otherwise, calculate `default` and use it.
			else
				this_sort_key, extra = generate_sortkey(lang, sc, sort_key, sort_base)
				default = this_sort_key
			end
			n = n + 1
			ret[n] = "[[Category:" .. category .. "|" .. this_sort_key .. "]]" .. (extra or "")
		end
	end
	format_categories = export.format_categories
end

--[==[
Add a "catfix", which is used on language-specific category pages to add language attributes and often script
classes to all entry names. The addition of language attributes and script classes makes the entry names display
better (using the language- or script-specific styles specified in [[MediaWiki:Common.css]]), which is particularly
important for non-English languages that do not have consistent font support in browsers.

Language attributes are added for all languages, but script classes are only added for languages with one script
listed in their data file, or for languages that have a default script listed in the {catfix_script} list in
[[Module:utilities/data]]. Some languages clearly have a default script, but still have other scripts listed in
their data file. If those other scripts are not simply scripts like {{cd|Brai}} (Braille), their default script
needs to be specified. Others do not have a default script.

* Serbo-Croatian is regularly written in both the Latin and Cyrillic scripts. Because it uses two scripts,
  Serbo-Croatian cannot have a script class applied to entries in its category pages, as only one script class
  can be specified at a time.
* German is usually written in the Latin script ({{cd|Latn}}), but Fraktur ({{cd|Latf}}) is also listed in
  its data file. So German needs an entry in the {catfix_script} list, so that the {{cd|Latn}} (Latin) script
  class will be applied to entries in its category pages.

To find the scripts listed for a language, go to [[Module:languages]] and use the search box to find the data file
for the language. To find out what a script code means, search the script code in [[Module:scripts/data]].]==]
function export.catfix(lang, sc)
	if not lang or not lang.getCanonicalName then
		error('The first argument to the function "catfix" should be a language object from [[Module:languages]] or [[Module:etymology languages]].')
	end
	if sc and not sc.getCode then
		error('The second argument to the function "catfix" should be a script object from [[Module:scripts]].')
	end

	-- To add script classes to links on pages created by category boilerplate templates.
	if not sc then
		local code = (catfix_scripts or get_catfix_scripts())[lang:getCode()] or catfix_scripts[lang:getFullCode()]
		if code then
			sc = get_script(code)
		end
	end

	-- If the language only has a single valid candidate script, apply it as the default.
	if not sc then
		local scripts = lang:getScripts()
		if #scripts == 1 then
			sc = scripts[1]
		else
			-- Iterate over scripts. If there is only one when ignoring
			-- scripts like Brai, then pick that one.
			for _, script in ipairs(scripts) do
				if script:getCode() ~= "Brai" then
					if sc then
						-- Multiple candidates - fail.
						sc = nil
						break
					else
						sc = script
					end
				end
			end
		end
	end
	
	-- Hack: using a <ul> tag prevents the parser from automatically generating a <p> tag around the catfix element.
	return "<ul class=\"catfix\" data-anchor=\"" ..
		anchor_encode(lang:getFullName()) .. "\">" ..
		tag_text("", lang, sc) .. "</ul>"
end

--[==[
Given a type (as a string) and an arbitrary number of entities, checks whether all of those entities are language,
family, script, writing system or Wikimedia language objects. Useful for error handling in functions that require
one of these kinds of object.

If `noErr` is set, the function returns false instead of throwing an error, which allows customised error handling to
be done in the calling function.]==]
function export.check_object(typ, noErr, ...)
	if ... == nil then
		if noErr then
			return false
		end
		error("Must provide at least one object to check.", 2)
	end
	for _, obj in ipairs{...} do
		if type(obj) ~= "table" or type(obj.hasType) ~= "function" then
			if noErr then
				return false
			end
			error("Function expected a " .. typ .. " object, but received a " .. type(obj) .. " instead.", 2)
		elseif not (typ == "object" or obj:hasType(typ)) then
			for _, wrong_type in ipairs{"family", "language", "script", "Wikimedia language", "writing system"} do
				if obj:hasType(wrong_type) then
					if noErr then
						return false
					end
					error("Function expected a " .. typ .. " object, but received a " .. wrong_type .. " object instead.", 2)
				end
			end
			if noErr then
				return false
			end
			error("Function expected a " .. typ .. " object, but received another type of object instead.", 2)
		end
	end
	return true
end

return export