Wiktionary gdwiktionary https://gd.wiktionary.org/wiki/Pr%C3%AComh-Dhuilleag MediaWiki 1.47.0-wmf.11 case-sensitive Meadhan Sònraichte Deasbaireachd Cleachdaiche Deasbaireachd a' chleachdaiche Wiktionary An deasbaireachd aig Wiktionary Faidhle Deasbaireachd an fhaidhle MediaWiki Deasbaireachd MediaWiki Teamplaid Deasbaireachd na teamplaid Cobhair Deasbaireachd na cobharach Roinn-seòrsa Deasbaireachd na roinn-seòrsa TimedText TimedText talk Mòideal Deasbaireachd mòideil Event Event talk Mòideal:debug 828 3741 86106 11070 2026-07-16T21:09:48Z Altronic 4137 Update module to the most recent version on English Wiktionary 86106 Scribunto text/plain local export = {} local string_utilities_module = "Module:string utilities" local table_module = "Module:table" local byte = string.byte local concat = table.concat local escape -- defined below local format = string.format local gsub = string.gsub local insert = table.insert local match = string.match local sub = string.sub local toNFC = mw.ustring.toNFC local function is_array(...) is_array = require(table_module).isArray return is_array(...) end local function isutf8(...) isutf8 = require(string_utilities_module).isutf8 return isutf8(...) end local function sorted_pairs(...) sorted_pairs = require(table_module).sortedPairs return sorted_pairs(...) end local function table_size(...) table_size = require(table_module).size return table_size(...) end do local escapes local function get_escapes() escapes, get_escapes = { ["\a"] = [[\a]], ["\b"] = [[\b]], ["\t"] = [[\t]], ["\n"] = [[\n]], ["\v"] = [[\v]], ["\f"] = [[\f]], ["\r"] = [[\r]], ["\""] = [[\"]], ["'"] = [[\']], ["\\"] = [[\\]], }, nil return escapes end local function escape_byte(ch) return (escapes or get_escapes())[ch] or format("\\%03d", byte(ch)) end local function escape_bytes(ch) return (gsub(ch, ".", escape_byte)) end local function escape_char(ch) local ch_len = #ch if ch_len == 1 then return escape_byte(ch) end local b = byte(ch) -- Matching bytes below \128 are all to be escaped, \128 to \191 can't -- be leading bytes in UTF-8, \192 and \193 could only occur in overlong -- encodings, so can't occur in UTF-8, U+0080 (\194\128) to U+009F -- (\194\159) are control characters, U+00A0 (\194\160) is the no-break -- space, and \245 to \255 could only occur in encodings for codepoints -- above U+10FFFF, so can't occur in UTF-8. if b < 194 or b > 244 or (b == 194 and byte(ch, 2) < 161) then return escape_bytes(ch) -- 2-byte encodings starting \194 to \223 are all valid, so no need to -- check them with isutf8(). If there are additional trailing -- bytes, escape them. elseif b < 224 then return ch_len == 2 and ch or (sub(ch, 1, 2) .. escape_bytes(sub(ch, 3))) end -- Check 3- and 4-byte encodings with isutf8(), as they might be -- invalid due to overlong encodings or being above U+10FFFF. As above, -- escape any additional trailing bytes. local n = b < 240 and 3 or 4 if ch_len == n then return isutf8(ch) and ch or escape_bytes(ch) elseif ch_len > n then local init_ch = sub(ch, 1, n) if isutf8(init_ch) then return init_ch .. escape_bytes(sub(ch, n + 1)) end end return escape_bytes(ch) end local function escape_non_NFC(str) local normalized = toNFC(str) if normalized == str then return str end local str_len, i, start, offset, output = #str, 1, 1, 0 while i <= str_len do local b = byte(str, i) if b == byte(normalized, i + offset) then i = i + 1 else if output == nil then output = {} end -- Backtrack to the start of the character. while b >= 128 and b < 192 do i = i - 1 b = byte(str, i) end -- Insert any intermediate characters up to this point. if start ~= i then insert(output, sub(str, start, i - 1)) end -- Get the first character, then find the sequence of characters -- which differs from the normalized string. local seq = match(str, "^.[\128-\191]*", i) -- Find the raw sequence and the normalized sequence by adding -- a character at a time to the raw sequence, and checking if -- it matches the current point in the normalized string. -- This is necessary to ensure that the offset between the two -- strings is correct, when comparing equivalent sections. local seq_len, poss_seq, norm_seq = #seq, seq while true do if not norm_seq then norm_seq = match(normalized, "^" .. toNFC(poss_seq), i + offset) -- Once a matching sequence has been found, check if it's -- still possible to match the same normalized sequence with -- a longer raw sequence, as form NFC will have taken the -- longest sequence when normalizing the input. elseif toNFC(poss_seq) ~= norm_seq then break end seq, seq_len = poss_seq, #poss_seq local nxt_ch = match(str, "^.[\128-\191]*", i + seq_len) if nxt_ch == nil then break end poss_seq = poss_seq .. nxt_ch end -- Modify the offset to account for the difference in length -- between the two sequences. Usually, the NFC form will be -- shorter, but in rare cases it is longer (e.g. U+0F73 -- normalizes to U+0F71 + U+0F72). offset = offset + #norm_seq - seq_len i = i + seq_len start = i -- Escape the non-ASCII portion of the sequence. This ensures -- that escapes added by escape_char don't end up double-escaped -- if they would otherwise be modified by form NFC; e.g. "\n" + -- U+0303 ("\ñ") needs to avoid escaping the "n". if seq ~= "" then insert(output, (gsub(seq, "[\128-\255]", escape_byte))) end end end if output == nil then return str end insert(output, sub(str, start)) return concat(output) end -- Escapes control characters, backslash, double quote, the no-break space, -- bytes that aren't used in UTF-8, invalid UTF-8 character sequences, and -- any bytes necessary to ensure that the output is Unicode form NFC, -- because MediaWiki automatically converts page content to form NFC; e.g. -- "e" + U+0301 ("é") results in "e\204\129", because otherwise the sequence -- would be converted to "é" (U+00E9)); this ensures that results can be -- relied upon to be stable if saved as part of page content. function export.escape(str) return escape_non_NFC(gsub(str, "[%c\"'\\\128-\255][\128-\191]*", escape_char)) end escape = export.escape end -- Convert a value to a string function export.dump(value, prefix, tsort) local t = type(value) prefix = prefix or "" if t == "string" then return '"' .. escape(value) .. '"' elseif t == "table" then local str_table = {} insert(str_table, " {") for key, val in sorted_pairs(value, tsort) do insert(str_table, " " .. prefix .. "\t[" .. export.dump(key, prefix .. "\t") .. "] = " .. gsub(export.dump(val, prefix .. "\t"), "^ ", "") .. ",") end insert(str_table, " " .. prefix .. "}") return concat(str_table, "\n") else return tostring(value) end end function export.highlight_dump(value, prefix, tsort, options) options = options or {} local func = options.modified and "modified_dump" or "dump" local dump = export[func](value, prefix, tsort) -- Remove spaces at beginnings of lines (which are simply to force a <pre></pre> tag). dump = gsub(dump, "%f[^%z\n] ", "") return export.highlight(dump) end -- Returns true if table contains a table as one of its values local function containsTable(t) for _, value in pairs(t) do if type(value) == "table" then return true end end return false end local function containsTablesWithSize(t, size) for _, value in pairs(t) do if type(value) == "table" and table_size(value) ~= size then return false end end return true end --[=[ Convert a value to a string. Like dump below, but if a table has consecutive numbered keys and does not have a table as one of its values, it will be placed on a single line. Used by [[Module:User:Erutuon/script recognition]]. ]=] function export.modified_dump(value, prefix, tsort) local t = type(value) prefix = prefix or "" if t == "string" then return '"' .. value .. '"' elseif t == "table" then local str_table = {} local containsTable = containsTable(value) local consecutive = is_array(value) if consecutive and not containsTable or containsTable and containsTablesWithSize(value, 3) then insert(str_table, "{") for key, val in sorted_pairs(value, tsort) do if containsTable then insert(str_table, "\n\t" .. prefix) else insert(str_table, " ") end if type(key) == "string" then insert(str_table, "[" .. export.modified_dump(key) .. "] = ") end insert(str_table, type(key) == "number" and type(val) == "number" and format("0x%05X", val) or export.modified_dump(val)) if not (consecutive and #value == 3) or type(key) == "number" and value[key + 1] then insert(str_table, ",") end end if containsTable then insert(str_table, "\n" .. prefix) else insert(str_table, " ") end insert(str_table, "}") return concat(str_table) end insert(str_table, " {") for key, val in sorted_pairs(value, tsort) do insert(str_table, " " .. prefix .. "\t[" .. export.modified_dump(key, prefix .. "\t") .. "] = " .. gsub(export.modified_dump(val, prefix .. "\t"), "^ ", "") .. ",") end insert(str_table, " " .. prefix .. "}") return concat(str_table, "\n") elseif t == "number" and value > 46 then return format("0x%05X", value) else return tostring(value) end end export.track = require("Module:debug/track") -- Trigger a script error from a template function export.error(frame) error(frame.args[1] or "(no message specified)") end --[[ Convenience function for generating syntaxhighlight tags. Display defaults to block. Options is a table. To display inline text with HTML highlighting: { inline = true, lang = "html" } ]] function export.highlight(content, options) if type(content) == "table" then options = content options = { lang = options.lang or "lua", inline = options.inline and true } return function(content) return mw.getCurrentFrame():extensionTag("syntaxhighlight", content, options) end else return mw.getCurrentFrame():extensionTag("syntaxhighlight", content, { lang = options and options.lang or "lua", inline = options and options.inline and true or nil }) end end function export.track_unrecognized_args(args, template_name) local function track(code) export.track(template_name .. "/" .. code) end track("unrecognized arg") local arg_list = {} for arg, value in pairs(args) do track("unrecognized arg/" .. arg) insert(arg_list, format("|%s=%s", arg, value)) end mw.log(format("Unrecognized parameter%s in {{%s}}: %s.", arg_list[2] and "s" or "", template_name, concat(arg_list, ", ") )) end do local placeholder = "_message_" function export._placeholder_error(frame) -- A dummy function that throws an error with a placeholder message. error(placeholder, (frame.args.level or 1) + 6) end -- Throw an error via callParserFunction, which generates a real error with traceback, automatic categorization in [[CAT:E]] etc., but the error message is returned as a string. Then, replace the placeholder error message with `message`, which is preprocessed. This is necessary when preprocessing needs to be applied (e.g. when using <pre> tags), since otherwise strip markers and other half-processed text gets displayed instead. function export.formatted_error(message, level) local frame = mw.getCurrentFrame() return (frame:callParserFunction("#invoke", {"debug", "_placeholder_error", level = level}) :gsub(placeholder, frame:preprocess(message))) end end return export flmvh7jakbzodr3hhecvopt1m6pdzph Mòideal:parameters 828 9197 86103 46504 2026-07-16T21:01:20Z Altronic 4137 Update module to the most recent version on English Wiktionary 86103 Scribunto text/plain --[==[TODO: * Change certain flag names, as some are misnomers: * Change `allow_holes` to `keep_holes`, because it's not the inverse of `disallow_holes`. * Change `allow_empty` to `keep_empty`, as it causes them to be kept as "" instead of deleted. * Sort out all the internal error calls. Manual error(format()) calls are used when certain parameters shouldn't be dumped, so find a way to avoid that. ]==] local export = {} local collation_module = "Module:collation" local families_module = "Module:families" local functions_module = "Module:fun" local gender_and_number_utilities_module = "Module:gender and number utilities" local labels_module = "Module:labels" local languages_module = "Module:languages" local math_module = "Module:math" local pages_module = "Module:pages" local parameters_finalize_set_module = "Module:parameters/finalizeSet" local parameters_track_module = "Module:parameters/track" local parse_utilities_module = "Module:parse utilities" local references_module = "Module:references" local scribunto_module = "Module:Scribunto" local scripts_module = "Module:scripts" local string_utilities_module = "Module:string utilities" local table_module = "Module:table" local wikimedia_languages_module = "Module:wikimedia languages" local yesno_module = "Module:yesno" local mw = mw local mw_title = mw.title local string = string local table = table local dump = mw.dumpObject local find = string.find local format = string.format local gsub = string.gsub local insert = table.insert local ipairs = ipairs local list_to_text = mw.text.listToText local make_title = mw_title.makeTitle local match = string.match local max = math.max local new_title = mw_title.new local next = next local pairs = pairs local pcall = pcall local require = require local sub = string.sub local tonumber = tonumber local type = type local unpack = unpack or table.unpack -- Lua 5.2 compatibility local current_title_text, current_namespace, sets -- Defined when needed. local namespaces = mw.site.namespaces --[==[ 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 extend(...) extend = require(table_module).extend return extend(...) end local function finalize_set(...) finalize_set = require(parameters_finalize_set_module) return finalize_set(...) end local function get_family_by_code(...) get_family_by_code = require(families_module).getByCode return get_family_by_code(...) end local function get_family_by_name(...) get_family_by_name = require(families_module).getByCanonicalName return get_family_by_name(...) end local function get_language_by_code(...) get_language_by_code = require(languages_module).getByCode return get_language_by_code(...) end local function get_language_by_name(...) get_language_by_name = require(languages_module).getByCanonicalName return get_language_by_name(...) end local function get_script_by_code(...) get_script_by_code = require(scripts_module).getByCode return get_script_by_code(...) end local function get_script_by_name(...) get_script_by_name = require(scripts_module).getByCanonicalName return get_script_by_name(...) end local function get_wm_lang_by_code(...) get_wm_lang_by_code = require(wikimedia_languages_module).getByCode return get_wm_lang_by_code(...) end local function get_wm_lang_by_code_with_fallback(...) get_wm_lang_by_code_with_fallback = require(wikimedia_languages_module).getByCodeWithFallback return get_wm_lang_by_code_with_fallback(...) end local function gsplit(...) gsplit = require(string_utilities_module).gsplit return gsplit(...) end local function is_callable(...) is_callable = require(functions_module).is_callable return is_callable(...) end local function is_integer(...) is_integer = require(math_module).is_integer return is_integer(...) end local function is_internal_title(...) is_internal_title = require(pages_module).is_internal_title return is_internal_title(...) end local function is_positive_integer(...) is_positive_integer = require(math_module).is_positive_integer return is_positive_integer(...) end local function iterate_list(...) iterate_list = require(table_module).iterateList return iterate_list(...) end local function num_keys(...) num_keys = require(table_module).numKeys return num_keys(...) end local function parse_gender_and_number_spec(...) parse_gender_and_number_spec = require(gender_and_number_utilities_module).parse_gender_and_number_spec return parse_gender_and_number_spec(...) end local function parse_references(...) parse_references = require(references_module).parse_references return parse_references(...) end local function pattern_escape(...) pattern_escape = require(string_utilities_module).pattern_escape return pattern_escape(...) end local function php_trim(...) php_trim = require(scribunto_module).php_trim return php_trim(...) end local function scribunto_parameter_key(...) scribunto_parameter_key = require(scribunto_module).scribunto_parameter_key return scribunto_parameter_key(...) end local function sort(...) sort = require(collation_module).sort return sort(...) end local function sorted_pairs(...) sorted_pairs = require(table_module).sortedPairs return sorted_pairs(...) end local function split(...) split = require(string_utilities_module).split return split(...) end local function split_labels_on_comma(...) split_labels_on_comma = require(labels_module).split_labels_on_comma return split_labels_on_comma(...) end local function split_on_comma(...) split_on_comma = require(parse_utilities_module).split_on_comma return split_on_comma(...) end local function tonumber_extended(...) tonumber_extended = require(math_module).tonumber_extended return tonumber_extended(...) end local function track(...) track = require(parameters_track_module) return track(...) end local function yesno(...) yesno = require(yesno_module) return yesno(...) end --[==[ intro: This module is used to standardize template argument processing and checking. A typical workflow is as follows (based on [[Module:translations]]): { ... local parent_args = frame:getParent().args local params = { [1] = {required = true, type = "language", default = "und"}, [2] = true, [3] = {list = true}, ["alt"] = true, ["id"] = true, ["sc"] = {type = "script"}, ["tr"] = true, ["ts"] = true, ["lit"] = true, } local args = require("Module:parameters").process(parent_args, params) -- Do further processing of the parsed arguments in `args`. ... } The `params` table should have the parameter names as the keys, and a (possibly empty) table of parameter tags as the value. An empty table as the value merely states that the parameter exists, but should not receive any special treatment; if desired, empty tables can be replaced with the value `true` as a perforamnce optimization. Possible parameter tags are listed below: ; {required = true} : The parameter is required; an error is shown if it is not present. The template's page itself is an exception; no error is shown there. ; {default =} : Specifies a default input value for the parameter, if it is absent or empty. This will be processed as though it were the input instead, so (for example) {default = "und"} with the type {"language"} will return a language object for [[:Category:Undetermined language|Undetermined language]] if no language code is provided. When used on list parameters, this specifies a default value for the first item in the list only. Note that it is not possible to generate a default that depends on the value of other parameters. If used together with {required = true}, the default applies only to template pages (see the following entry), as a side effect of the fact that "required" parameters aren't actually required on template pages. This can be used to show an example of the template in action when the template page is visited; however, it is preferred to use `template_default` for this purpose, for clarity. ; {template_default =} : Specifies a default input value for absent or empty parameters only on the template demo invocation (the invocation of the template that is displayed when the template page that implements the template is viewed). Template pages are pages in template space that invoke (through {{tl|#invoke:}}) the module that implements the template and calls [[Module:parameters]]. For example, the page [[Template:en-noun]] implements the {{tl|en-noun}} template, which in turn invokes [[Module:en-headword]], and is a template page for [[Module:en-headword]]. When the template page [[Template:en-noun]] is visited, the {{tl|#invoke:}} of the template's module is expanded as if the template were called without arguments, and the output is inserted at that point into the processed page. This output serves as a sort of demo of the template's functionality. `template_default` can be used to supply default values for use only in this demo. Since the template page may also contain other invocations of the same template (e.g. on the template's documentation page, which is typically transcluded into the template page itself), `template_default` does not apply if there are any arguments passed to the template or if the template is invoked on any other page but its own template page (which is checked by comparing the name of the invoking template to the current pagename). Both `template_default` and `default` can be specified for the same parameter. If this is done, `template_default` applies for the argumentless template invocation on the template page, and `default` in all other circumstances As an example, {{tl|cs-IPA}} uses the equivalent of {[1] = {default = "+", template_default = "příklad"}} to supply a default of {"+"} for mainspace and documentation pages (which tells the module to use the value of the {{para|pagename}} parameter, falling back to the actual pagename), but {"příklad"} (which means "example"), on [[Template:cs-IPA]]. ; {alias_of =} : Treat the parameter as an alias of another. When arguments are specified for this parameter, they will automatically be renamed and stored under the alias name. This allows for parameters with multiple alternative names, while still treating them as if they had only one name. The conversion-related properties of an aliased parameter (e.g. `type`, `set`, `convert`, `sublist`) are taken from the aliasee, and the corrresponding properties set on the alias itself are ignored; but other properties on the alias are taken from the alias's spec and not from the aliasee's spec. This means, for example, that if you create an alias of a list parameter, the alias must also specify the `list` property or it is not a list. (In such a case, a value specified for the alias goes into the first item of the aliasee's list. You cannot make a list alias of a non-list parameter; this causes an error to be thrown.) Similarly, if you specify `separate_no_index` on an aliasee but not on the alias, uses of the unindexed aliasee parameter are stored into the `.default` key, but uses of the unindexed alias are stored into the first numbered key of the aliasee's list. Aliases cannot be required, as this prevents the other name or names of the parameter from being used. Parameters that are aliases and required at the same time cause an error to be thrown. ; {allow_empty = true} : If the argument is an empty string value, it is not converted to {nil}, but kept as-is. The use of `allow_empty` is disallowed if a type has been specified, and causes an error to be thrown. ; {no_trim = true} : Spacing characters such as spaces and newlines at the beginning and end of a positional parameter are not removed. (MediaWiki itself automatically trims spaces and newlines at the edge of named parameters.) The use of `no_trim` is disallowed if a type has been specified, and causes an error to be thrown. ; {type =} : Specifies what value type to convert the argument into. The default is to leave it as a text string. Alternatives are: :; {type = "boolean"} :: The value is treated as a boolean value, either true or false. No value, the empty string, and the strings {"0"}, {"no"}, {"n"}, {"false"}, {"f"} and {"off"} are treated as {false}, all other values are considered {true}. :; {type = "number"} :: The value is converted into a number, and throws an error if the value is not parsable as a number. Input values may be signed (`+` or `-`), and may contain decimal points and leading zeroes. If {allow_hex = true}, then hexadecimal values in the form {"0x100"} may optionally be used instead, which otherwise have the same syntax restrictions (including signs, decimal digits, and leading zeroes after {"0x"}). Hexadecimal inputs are not case-sensitive. Lua's special number values (`inf` and `nan`) are not possible inputs. :; {type = "range"} :: The value is interpreted as a hyphen-separated range of two numbers (e.g. {"2-4"} is interpreted as the range from {2} to {4}). A number input without a hyphen is interpreted as a range from that number to itself (e.g. the input {"1"} is interpreted as the range from {1} to {1}). Any optional flags which are available for numbers will also work for ranges. :; {type = "language"} :: The value is interpreted as a full or [[Wiktionary:Languages#Etymology-only languages|etymology-only language]] code language code (or name, if {method = "name"}) and converted into the corresponding object (see [[Module:languages]]). If the code or name is invalid, then an error is thrown. The additional setting {family = true} can be given to allow [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned. Note that to distinguish an etymology-only language object from a full language object, use {object:hasType("language", "etymology-only")}. :; {type = "full language"} :: The value is interpreted as a full language code (or name, if {method = "name"}) and converted into the corresponding object (see [[Module:languages]]). If the code or name is invalid, then an error is thrown. Etymology-only languages are not allowed. The additional setting {family = true} can be given to allow [[Wiktionary:Language families|language family codes]] to be considered valid and the corresponding object returned. :; {type = "Wikimedia language"} :: The value is interpreted as a code and converted into a Wikimedia language object. If the code is invalid, then an error is thrown. If {fallback = true} is specified, conventional language codes which are different from their Wikimedia equivalent will also be accepted as a fallback. :; {type = "family"} :: The value is interpreted as a language family code (or name, if {method = "name"}) and converted into the corresponding object (see [[Module:families]]). If the code or name is invalid, then an error is thrown. :; {type = "script"} :: The value is interpreted as a script code (or name, if {method = "name"}) and converted into the corresponding object (see [[Module:scripts]]). If the code or name is invalid, then an error is thrown. :; {type = "title"} :: The value is interpreted as a page title and converted into the corresponding object (see the [[mw:Extension:Scribunto/Lua_reference_manual#Title_library|Title library]]). If the page title is invalid, then an error is thrown; by default, external titles (i.e. those on other wikis) are not treated as valid. Options are: ::; {namespace = n} ::: The default namespace, where {n} is a namespace number; this is treated as {0} (the mainspace) if not specified. ::; {allow_external = true} ::: External titles are treated as valid. ::; {prefix = "namespace override"} (default) ::: The default namespace prefix will be prefixed to the value is already prefixed by a namespace prefix. For instance, the input {"Foo"} with namespace {10} returns {"Template:Foo"}, {"Wiktionary:Foo"} returns {"Wiktionary:Foo"}, and {"Template:Foo"} returns {"Template:Foo"}. Interwiki prefixes cannot act as overrides, however: the input {"fr:Foo"} returns {"Template:fr:Foo"}. ::; {prefix = "force"} ::: The default namespace prefix will be prefixed unconditionally, even if the value already appears to be prefixed. This is the way that {{tl|#invoke:}} works when calling modules from the module namespace ({828}): the input {"Foo"} returns {"Module:Foo"}, {"Wiktionary:Foo"} returns {"Module:Wiktionary:Foo"}, and {"Module:Foo"} returns {"Module:Module:Foo"}. ::; {prefix = "full override"} ::: The same as {prefix = "namespace override"}, except that interwiki prefixes can also act as overrides. For instance, {"el:All topics"} with namespace {14} returns {"el:Category:All topics"}. Due to the limitations of MediaWiki, only the first prefix in the value may act as an override, so the namespace cannot be overridden if the first prefix is an interwiki prefix: e.g. {"el:Template:All topics"} with namespace {14} returns {"el:Category:Template:All topics"}. :; {type = "parameter"} :: The value is interpreted as the name of a parameter, and will be normalized using the method that Scribunto uses when constructing a {frame.args} table of arguments. This means that integers will be converted to numbers, but all other arguments will remain as strings (e.g. {"1"} will be normalized to {1}, but {"foo"} and {"1.5"} will remain unchanged). Note that Scribunto also trims parameter names, following the same trimming method that this module applies by default to all parameter types. :: This type is useful when one set of input arguments is used to construct a {params} table for use in a subsequent {export.process()} call with another set of input arguments; for instance, the set of valid parameters for a template might be defined as {{tl|#invoke:[some module]|args=}} in the template, where {args} is a sublist of valid parameters for the template. :; {type = "qualifier"} :: The value is interpreted as a qualifier and converted into the correct format for passing into `format_qualifiers()` in [[Module:qualifier]] (which currently just means converting it to a one-item list). :; {type = "labels"} :: The value is interpreted as a comma-separated list of labels and converted into the correct format for passing into `show_labels()` in [[Module:labels]] (which is currently a list of strings). Splitting is done on commas not followed by whitespace, except that commas inside of double angle brackets do not count even if not followed by whitespace. This type should be used by for normal labels (typically specified using {{para|l}} or {{para|ll}}) and accent qualifiers (typically specified using {{para|a}} and {{para|aa}}). :; {type = "references"} :: The value is interpreted as one or more references, in the format prescribed by `parse_references()` in [[Module:references]], and converted into a list of objects of the form accepted by `format_references()` in the same module. If a syntax error is found in the reference format, an error is thrown. :; {type = "genders"} :: The value is interpreted as one or more comma-separated gender/number specs, in the format prescribed by [[Module:gender and number]]. Inline modifiers (`<q:...>`, `<qq:...>`, `<l:...>`, `<ll:...>` or `<ref:...>`) may be attached to a gender/number spec. :; {type = "form of tags"} :: The value is interpreted as an ampersand-separated list of grammar tags and converted into the correct format for passing as `tags` into `tagged_inflections()` in [[Module:form of]] (which is currently a list of strings). Splitting is always done by ampersands. This type should be used by for inflection qualifiers that act as grammar tags (typically specified using {{para|infl}}). :; {type = function(val) ... end} :: `type` may be set to a function (or callable table), which must take the argument value as its sole argument, and must output one of the other recognized types. This is particularly useful for lists (see below), where certain values need to be interpreted differently to others. ; {list =} : Treat the parameter as a list of values, each having its own parameter name, rather than a single value. The parameters will have a number at the end, except optionally for the first (but see also {require_index = true}). For example, {list = true} on a parameter named "head" will include the parameters {{para|head}} (or {{para|head1}}), {{para|head2}}, {{para|head3}} and so on. If the parameter name is a number, another number doesn't get appended, but the counting simply continues, e.g. for parameter {3} the sequence is {{para|3}}, {{para|4}}, {{para|5}} etc. List parameters are returned as numbered lists, so for a template that is given the parameters `|head=a|head2=b|head3=c`, the processed value of the parameter {"head"} will be { { "a", "b", "c" }}}. : The value for {list =} can also be a string. This tells the module that parameters other than the first should have a different name, which is useful when the first parameter in a list is a number, but the remainder is named. An example would be for genders: {list = "g"} on a parameter named {1} would have parameters {{para|1}}, {{para|g2}}, {{para|g3}} etc. : If the number is not located at the end, it can be specified by putting {"\1"} at the number position. For example, parameters {{para|f1accel}}, {{para|f2accel}}, ... can be captured by using the parameter name {"f\1accel"}, as is done in [[Module:headword/templates]]. ; {set =} : Require that the value of the parameter be one of the specified values (or omitted, if {required = true} isn't given). Two formats are allowed; either a list of possible values can be supplied, or a table can be supplied where the keys are allowed values and the values are either `true` or a string naming a value found elsewhere in the table as a key. In the latter case, the key is an alias and the value is the canonical value, and if the user uses the alias, it will automatically be mapped to the canonical value. In such a case, the canonical value cannot itself be an alias. The use of `set` is disallowed if {type = "boolean"} and causes an error to be thrown. ; {sublist =} : The value of the parameter is a delimiter-separated list of individual raw values. The resulting field in `args` will be a Lua list (i.e. a table with numeric indices) of the converted values. If {sublist = true} is given, the values will be split on commas (possibly with whitespace on one or both sides of the comma, which is ignored). If {sublist = "comma without whitespace"} is given, the values will be split on commas which are not followed by whitespace, and which aren't preceded by an escaping backslash. Otherwise, the value of `sublist` should be either a Lua pattern specifying the delimiter(s) to split on or a function (or callable table) to do the splitting, which is passed two values (the value to split and a function to signal an error) and should return a list of the split values. ; {convert =} : If given, this specifies a function (or callable table) to convert the raw parameter value into the Lua object used during further processing. The function is passed two arguments, the raw parameter value itself and a function used to signal an error during parsing or conversion, and should return one value, the converted parameter. The error-signaling function contains the name and raw value of the parameter embedded into the message it generates, so these do not need to specified in the message passed into it. If `type` is specified in conjunction with `convert`, the processing by `type` happens first. If `sublist` is given in conjunction with `convert`, the raw parameter value will be split appropriately and `convert` called on each resulting item. ; {allow_hex = true} : When used in conjunction with {type = "number"}, allows hexadecimal numbers as inputs, in the format {"0x100"} (which is not case-sensitive). ; {family = true} : When used in conjunction with {type = "language"}, allows [[Wiktionary:Language families|language family codes]] to be returned. To check if a given object refers to a language family, use {object:hasType("family")}. ; {method = "name"} : When used in conjunction with {type = "language"}, {type = "family"} or {type = "script"}, checks for and parses a language, family or script name instead of a code. ; {allow_holes = true} : This is used in conjunction with list-type parameters. By default, the values are tightly packed in the resulting list. This means that if, for example, an entry specified `head=a|head3=c` but not {{para|head2}}, the returned list will be { {"a", "c"}}}, with the values stored at the indices {1} and {2}, not {1} and {3}. If it is desirable to keep the numbering intact, for example if the numbers of several list parameters correlate with each other (like those of {{tl|affix}}), then this tag should be specified. : If {allow_holes = true} is given, there may be {nil} values in between two real values, which makes many of Lua's table processing functions no longer work, like {#} or {ipairs()}. To remedy this, the resulting table will contain an additional named value, `maxindex`, which tells you the highest numeric index that is present in the table. In the example above, the resulting table will now be { { "a", nil, "c", maxindex = 3}}}. That way, you can iterate over the values from {1} to `maxindex`, while skipping {nil} values in between. ; {disallow_holes = true} : This is used in conjunction with list-type parameters. As mentioned above, normally if there is a hole in the source arguments, e.g. `head=a|head3=c` but not {{para|head2}}, it will be removed in the returned list. If {disallow_holes = true} is specified, however, an error is thrown in such a case. This should be used whenever there are multiple list-type parameters that need to line up (e.g. both {{para|head}} and {{para|tr}} are available and {{para|head3}} lines up with {{para|tr3}}), unless {allow_holes = true} is given and you are prepared to handle the holes in the returned lists. ; {disallow_missing = true} : This is similar to {disallow_holes = true}, but an error will not be thrown if an argument is blank, rather than completely missing. This may be used to tolerate intermediate blank numerical parameters, which sometimes occur in list templates. For instance, `head=a|head2=|head3=c` will not throw an error, but `head=a|head3=c` will. ; {require_index = true} : This is used in conjunction with list-type parameters. By default, the first parameter can have its index omitted. For example, a list parameter named `head` can have its first parameter specified as either {{para|head}} or {{para|head1}}. If {require_index = true} is specified, however, only {{para|head1}} is recognized, and {{para|head}} will be treated as an unknown parameter. {{tl|affixusex}} (and variants {{tl|suffixusex}}, {{tl|prefixusex}}) use this, for example, on all list parameters. ; {separate_no_index = true} : This is used to distinguish between {{para|head}} and {{para|head1}} as different parameters. For example, in {{tl|affixusex}}, to distinguish between {{para|sc}} (a script code for all elements in the usex's language) and {{para|sc1}} (the script code of the first element, used when the first element is prefixed with a language code to indicate that it is in a different language). When this is used, the resulting table will contain an additional named value, `default`, which contains the value for the indexless argument. ; {flatten = true} : This is used in conjunction with list-type parameters when `sublist` or a list-generating type such as {"labels"} or {"genders"} is also specified, and causes the resulting list to be flattened. Not currently compatible with {allow_holes = true}. ; {replaced_by =} : Specifies that the parameter is no longer valid, and has been replaced by some other mechanism. If the value of `replaced_by` is a string, it is the name of the new parameter to use instead. Use the `reason` tag to specify the reason why this change has been made, e.g. {reason = "for consistency with the corresponding parameter in other Romance-language headword templates"}. If the value of `replaced_by` is {false}, there is no replacement parameter. In this case, `instead` should be supplied with a description of what to do instead, e.g. {instead = "use an inline modifier on |2= such as <q:...>, <qq:...>, <l:...> or <ll:...>"}. You can also supply a justification in `reason` if you feel it is appropriate or necessary to do so. ; {reason =} : When used in conjunction with `replaced_by`, specifies the reason for the parameter replacement. ; {instead =} : When used in conjunction with {replaced_by = false}, specifies what to do instead of using the removed parameter. ; {demo = true} : This is used as a way to ensure that the parameter is only enabled on the template's own page (and its documentation page), and in the User: namespace; otherwise, it will be treated as an unknown parameter. This should only be used if special settings are required to showcase a template in its documentation (e.g. adjusting the pagename or disabling categorization). In most cases, it should be possible to do this without using demo parameters, but they may be required if a template/documentation page also contains real uses of the same template as well (e.g. {{tl|shortcut}}), as a way to distinguish them. ; {deprecated = true} : This is for tracking the use of deprecated parameters, including any aliases that are being brought out of use. See [[Wiktionary:Tracking]] for more information. ]==] -- Returns true if the current page is a template or module containing the current {{#invoke}}. -- If the include_documentation argument is given, also returns true if the current page is either page's documentation page. local own_page, own_page_or_documentation local function is_own_page(include_documentation) if own_page == nil then if current_namespace == nil then local current_title = mw_title.getCurrentTitle() current_title_text, current_namespace = current_title.prefixedText, current_title.namespace end local frame = current_namespace == 828 and mw.getCurrentFrame() or current_namespace == 10 and mw.getCurrentFrame():getParent() if frame then local frame_title_text = frame:getTitle() own_page = current_title_text == frame_title_text own_page_or_documentation = own_page or current_title_text == frame_title_text .. "/documentation" else own_page, own_page_or_documentation = false, false end end return include_documentation and own_page_or_documentation or own_page end -------------------------------------- Some helper functions ----------------------------- -- Convert a list in `list` to a string, separating the final element from the preceding one(s) by `conjunction`. If -- `dump_vals` is given, pass all values in `list` through mw.dumpObject() (WARNING: this destructively modifies -- `list`). This is similar to serialCommaJoin() in [[Module:table]] when used with the `dontTag = true` option, but -- internally uses mw.text.listToText(). local function concat_list(list, conjunction, dump_vals) if dump_vals then for k, v in pairs(list) do list[k] = dump(v) end end return list_to_text(list, nil, conjunction) end -- A helper function for use with generating error-signaling functions in the presence of raw value conversion. Format a -- message `msg`, including the processed value `processed` if it is different from the raw value `rawval`; otherwise, -- just return `msg`. local function msg_with_processed(msg, rawval, processed) if rawval == processed then return msg end local processed_type = type(processed) return format("%s (processed value %s)", msg, (processed_type == "string" or processed_type == "number") and processed or dump(processed) ) end -- Separate form of tags with ampersand (&). local function split_tags_on_ampersand(tags) return split(tags, "&") end -------------------------------------- Error handling ----------------------------- local function process_error(fmt, ...) local args = {...} for i, val in ipairs(args) do args[i] = dump(val) end if type(fmt) == "table" then -- hacky signal that we're called from internal_process_error(), and not to omit stack frames return error(format(fmt[1], unpack(args))) end return error(format(fmt, unpack(args)), 3) end local function internal_process_error(fmt, ...) process_error({"Internal error in `params` table: " .. fmt}, ...) end -- Check that a parameter or argument is in the form form Scribunto normalizes input argument keys into (e.g. 1 not "1", "foo" not " foo "). Otherwise, it won't be possible to normalize inputs in the expected way. Unless is_argument is set, also check that the name only contains one placeholder at most, and that strings don't resolve to numeric keys once the placeholder has been substituted. local function validate_name(name, desc, extra_name, is_argument) local normalized = scribunto_parameter_key(name) if name and name == normalized then if is_argument or type(name) ~= "string" then return end local placeholder = find(name, "\1", nil, true) if not placeholder then return elseif find(name, "\1", placeholder + 1, true) then error(format( "Internal error: expected %s to only contain one placeholder, but saw %s", extra_name and (desc .. dump(extra_name)) or desc, dump(name) )) end local first_name = gsub(name, "\1", "1") normalized = scribunto_parameter_key(first_name) if first_name == normalized then return end error(format( "Internal error: %s cannot resolve to numeric parameters once any placeholder has been substituted, but %s resolves to %s", extra_name and (desc .. dump(extra_name)) or desc, dump(name), dump(normalized) )) elseif normalized == nil then error(format( "Internal error: expected %s to be of type string or number, but saw %s", extra_name and (desc .. dump(extra_name)) or desc, type(name) )) end error(format( "Internal error: expected %s to be Scribunto-compatible: %s (a %s) should be %s (a %s)", extra_name and (desc .. dump(extra_name)) or desc, dump(name), type(name), dump(normalized), type(normalized) )) end local function validate_alias_options(...) local invalid = { required = true, default = true, template_default = true, allow_holes = true, disallow_holes = true, disallow_missing = true, } function validate_alias_options(param, name, main_param, alias_of) for k in pairs(param) do if invalid[k] then track("bad alias option") -- internal_process_error( -- "parameter %s cannot have the option %s, as it is an alias of parameter %s.", -- name, option, alias_of -- ) end end -- Soon, aliases will inherit options from the main parameter via __index. Track cases where this would happen. if main_param ~= true then for k in pairs(main_param) do if param[k] == nil and not invalid[k] then if k == "list" then -- these need to be changed to list = false to retain current behaviour track("mismatched list alias option") elseif not (k == "type" or k == "set" or k == "sublist") then -- rarely specified on aliases, as they're effectively inherited already track("mismatched alias option") end end end end end validate_alias_options(...) end -- TODO: give ranges instead of long lists, if possible. --[==[ func: export.params_list_error(params, msg) Given a key-value table of raw parameters `params`, display an error message about all the parameters seen in the table. The parameter names are displayed in sorted order. `msg` should be e.g. {"required"} or {"not used by this template"}. This is used internally to display error messages about required or invalid parameters, and can be used for the same purpose by code that processes its own parameters (e.g. if the `return_unknown` flag is specified to `process`). ]==] local function params_list_error(params, msg) local list, n = {}, 0 for name in sorted_pairs(params) do n = n + 1 list[n] = name end error(format( "Parameter%s %s.", format(n == 1 and " %s is" or "s %s are", concat_list(list, " and ", true)), msg ), 3) end export.params_list_error = params_list_error -- Helper function for use with convert_val_error(). Format a list of possible choices using `concat_list` and -- conjunction "or", displaying "either " before the choices if there's more than one. local function format_choice_list(valid) return (#valid > 1 and "either " or "") .. concat_list(valid, " or ") end -- Signal an error for a value `val` that is not of the right type `valid` (which is either a string specifying a type, or -- a list of possible values, in the case where `set` was used). `name` is the name of the parameter and can be a -- function to signal an error (which is assumed to automatically display the parameter's name and value). `seetext` is -- an optional additional explanatory link to display (e.g. [[WT:LOL]], the list of possible languages and codes). local function convert_val_error(val, name, valid, seetext) if is_callable(name) then if type(valid) == "table" then valid = "choice, must be " .. format_choice_list(valid) end name(format("Invalid %s; the value %s is not valid%s", valid, val, seetext and "; see " .. seetext or "")) else if type(valid) == "table" then valid = format_choice_list(valid) else valid = "a valid " .. valid end error(format("Parameter %s must be %s; the value %s is not valid.%s", dump(name), valid, dump(val), seetext and " See " .. seetext .. "." or "")) end end -- Generate the appropriate error-signaling function given parameter value `val` and name `name`. If `name` is already -- a function, it is just returned; otherwise a function is generated and returned that displays the passed-in messaeg -- along with the parameter's name and value. local function make_parse_err(val, name) if is_callable(name) then return name end return function(msg) error(format("%s: parameter %s=%s", msg, name, val)) end end -------------------------------------- Value conversion ----------------------------- -- For a list parameter `name` and corresponding value `list_name` of the `list` field (which should have the same value -- as `name` if `list = true` was given), generate a pattern to match parameters of the list and store the pattern as a -- key in `patterns`, with corresponding value set to `name`. For example, if `list_name` is "tr", the pattern will -- match "tr" as well as "tr1", "tr2", ..., "tr10", "tr11", etc. If the `list_name` contains a \1 in it, the numeric -- portion goes in place of the \1. For example, if `list_name` is "f\1accel", the pattern will match "faccel", -- "f1accel", "f2accel", etc. Any \1 in `name` is removed before storing into `patterns`. local function save_pattern(name, list_name, patterns) name = type(name) == "string" and gsub(name, "\1", "") or name if find(list_name, "\1", nil, true) then patterns["^" .. gsub(pattern_escape(list_name), "\1", "([1-9]%%d*)") .. "$"] = name else patterns["^" .. pattern_escape(list_name) .. "([1-9]%d*)$"] = name list_name = list_name .. "\1" end validate_name(list_name, "the list field of parameter ", name) return patterns end -- A helper function for use with `sublist`. It is an iterator function for use in a for-loop that returns split -- elements of `val` using `sublist` (a Lua split pattern; boolean `true` to split on commas optionally surrounded by -- whitespace; "comma without whitespace" to split only on commas not followed by whitespace which have not been escaped -- by a backslash; or a function to do the splitting, which is passed two values, the value to split and a function to -- signal an error, and should return a list of the split elements). `name` is the parameter name or error-signaling -- function passed into convert_val(). local function split_sublist(val, name, sublist) if sublist == true then return gsplit(val, "%s*,%s*") -- Split an argument on comma, but not comma followed by whitespace. elseif sublist == "comma without whitespace" then -- If difficult cases, use split_on_comma. if find(val, "\\", nil, true) or match(val, ",%s") then return iterate_list(split_on_comma(val)) end -- Otherwise, use gsplit. return gsplit(val, ",") elseif type(sublist) == "string" then return gsplit(val, sublist) elseif not is_callable(sublist) then error(format('Internal error: expected `sublist` to be of type "string" or "function" or boolean `true`, but saw %s', dump(sublist))) end return iterate_list(sublist(val, make_parse_err(val, name))) end -- For parameter named `name` with value `val` and param spec `param`, if the `set` field is specified, verify that the -- value is one of the one specified in `set`, and throw an error otherwise. `name` is taken directly from the -- corresponding parameter passed into convert_val() and may be a function to signal an error. Optional `param_type` is -- a string specifying the conversion type of `val` and is used for special-casing: If `param_type` is "boolean", an -- internal error is thrown (since `set` cannot be used in conjunction with booleans) and if `param_type` is "number", -- no checking happens because in this case `set` contains numbers and is checked inside the number conversion function -- itself, after converting `val` to a number. Return the canonical value of `val` (which may be different from `val` -- if an alias map is given). local function check_set(val, name, param, param_type) if param_type == "boolean" then error(format('Internal error: cannot use `set` with `type = "%s"`', param_type)) -- Needs to be special cased because the check happens after conversion to numbers. elseif param_type == "number" then return val end local set, map = param.set if sets == nil then map = finalize_set(set, name) sets = {[set] = map} else map = sets[set] if map == nil then map = finalize_set(set, name) sets[set] = map end end local newval = map[val] if newval == true then return val elseif newval ~= nil then return newval end local list = {} for k, v in sorted_pairs(map) do if v == true then insert(list, dump(k)) else insert(list, ("%s (alias of %s)"):format(dump(k), dump(v))) end end -- If the parameter is not required then put "or empty" at the end of the list, to avoid implying the parameter is actually required. if not param.required then insert(list, "empty") end convert_val_error(val, name, list) end local function convert_language(val, name, param, allow_etym) local method, func = param.method if method == nil or method == "code" then func, method = get_language_by_code, "code" elseif method == "name" then func, method = get_language_by_name, "name" else error(format('Internal error: expected `method` for type `language` to be "code", "name" or undefined, but saw %s', dump(method))) end local lang = func(val, nil, allow_etym, param.family) if lang then return lang end local list, links = {"language"}, {"[[WT:LOL]]"} if allow_etym then insert(list, "etymology language") insert(links, "[[WT:LOL/E]]") end if param.family then insert(list, "family") insert(links, "[[WT:LOF]]") end convert_val_error(val, name, concat_list(list, " or ") .. " " .. (method == "name" and "name" or "code"), concat_list(links, " and ")) end local function convert_number(val, allow_hex) -- Call tonumber_extended with the `real_finite` flag, which filters out ±infinity and NaN. -- By default, specify base 10, which prevents 0x hex inputs from being converted. -- If `allow_hex` is set, then don't give a base, which means 0x hex inputs will work. local num = tonumber_extended(val, not allow_hex and 10 or nil, "finite_real") if not num then return num end if match(val, "[eEpP.]") then -- float track("number not an integer") end if find(val, "+", nil, true) then track("number with +") end -- Track various unusual number inputs to determine if it should be restricted to positive integers by default (possibly including 0). if not is_positive_integer(num) then track("number not a positive integer") if num == 0 then track("number is 0") elseif not is_integer(num) then track("number not an integer") end end return num end -- TODO: validate parameter specs separately, as it's making the handler code really messy at the moment. local type_handlers = setmetatable({ ["boolean"] = function(val) return yesno(val, true) end, ["family"] = function(val, name, param) local method, func = param.method if method == nil or method == "code" then func, method = get_family_by_code, "code" elseif method == "name" then func, method = get_family_by_name, "name" else error(format('Internal error: expected `method` for type `family` to be "code", "name" or undefined, but saw %s', dump(method))) end return func(val) or convert_val_error(val, name, "family " .. method, "[[WT:LOF]]") end, ["labels"] = function(val, name, param) -- FIXME: Should be able to pass in a parse_err function. return split_labels_on_comma(val) end, ["form of tags"] = function(val, name, param) return split_tags_on_ampersand(val) end, ["language"] = function(val, name, param) return convert_language(val, name, param, true) end, ["full language"] = convert_language, ["number"] = function(val, name, param) local allow_hex = param.allow_hex if allow_hex and allow_hex ~= true then error(format( 'Internal error: expected `allow_hex` for type `number` to be of type "boolean" or undefined, but saw %s', dump(allow_hex) )) end local num = convert_number(val, allow_hex) if param.set then -- Don't pass in "number" here; otherwise no checking will happen. num = check_set(num, name, param) end if num then return num end convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number") end, ["range"] = function(val, name, param) local allow_hex = param.allow_hex if allow_hex and allow_hex ~= true then error(format( 'Internal error: expected `allow_hex` for type `range` to be of type "boolean" or undefined, but saw %s', dump(allow_hex) )) end -- Pattern ensures leading minus signs are accounted for. local m1, m2 = match(val, "^(%s*%S.-)%-(%s*%S.*)") if m1 then m1 = convert_number(m1, allow_hex) if m1 then m2 = convert_number(m2, allow_hex) if m2 then return {m1, m2} end end end -- Try `val` if it couldn't be split into a range, and return a range of `val` to `val` if possible. local num = convert_number(val, allow_hex) if num then return {num, num} end convert_val_error(val, name, (allow_hex and "decimal or hexadecimal " or "") .. "number or a hyphen-separated range of two numbers") end, ["parameter"] = function(val, name, param) -- Use the `no_trim` option, as any trimming will have already been done. return scribunto_parameter_key(val, true) end, ["qualifier"] = function(val, name, param) return {val} end, ["references"] = function(val, name, param) return parse_references(val, make_parse_err(val, name)) end, ["genders"] = function(val, name, param) if not val:find("[,<]") then return {{spec = val}} end -- NOTE: We don't pass in allow_space_around_comma. Consistent with other comma-separated types, there shouldn't -- be spaces around the comma. return parse_gender_and_number_spec { spec = val, parse_err = make_parse_err(val, name), allow_multiple = true, } end, ["script"] = function(val, name, param) local method, func = param.method if method == nil or method == "code" then func, method = get_script_by_code, "code" elseif method == "name" then func, method = get_script_by_name, "name" else error(format('Internal error: expected `method` for type `script` to be "code", "name" or undefined, but saw %s', dump(method))) end return func(val) or convert_val_error(val, name, "script " .. method, "[[WT:LOS]]") end, ["string"] = function(val, name, param) -- To be removed as unnecessary. track("string") return val end, -- TODO: add support for resolving to unsupported titles. -- TODO: split this into "page name" (i.e. internal) and "link target" (i.e. external as well), which is more intuitive. ["title"] = function(val, name, param) local namespace = param.namespace if namespace == nil then namespace = 0 else local valid_type = type(namespace) ~= "number" and 'of type "number" or undefined' or not namespaces[namespace] and "a valid namespace number" or nil if valid_type then error(format('Internal error: expected `namespace` for type `title` to be %s, but saw %s', valid_type, dump(namespace))) end end -- Decode entities. WARNING: mw.title.makeTitle must be called with `decoded` (as it doesn't decode) and mw.title.new must be called with `val` (as it does decode, so double-decoding needs to be avoided). local decoded, prefix, title = decode_entities(val), param.prefix -- If the input is a fragment, treat the title as the current title with the input fragment. if sub(decoded, 1, 1) == "#" then -- If prefix is "force", only get the current title if it's in the specified namespace. current_title includes the namespace prefix. if current_namespace == nil then local current_title = mw_title.getCurrentTitle() current_title_text, current_namespace = current_title.prefixedText, current_title.namespace end if not (prefix == "force" and namespace ~= current_namespace) then title = new_title(current_title_text .. val) end elseif prefix == "force" then -- Unconditionally add the namespace prefix (mw.title.makeTitle). title = make_title(namespace, decoded) elseif prefix == "full override" then -- The first input prefix will be used as an override (mw.title.new). This can be a namespace or interwiki prefix. title = new_title(val, namespace) elseif prefix == nil or prefix == "namespace override" then -- Only allow namespace prefixes to override. Interwiki prefixes therefore need to be treated as plaintext (e.g. "el:All topics" with namespace 14 returns "el:Category:All topics", but we want "Category:el:All topics" instead; if the former is really needed, then the input ":el:Category:All topics" will work, as the initial colon overrides the namespace). mw.title.new can take namespace names as well as numbers in the second argument, and will throw an error if the input isn't a valid namespace, so this can be used to determine if a prefix is for a namespace, since mw.title.new will return successfully only if there's either no prefix or the prefix is for a valid namespace (in which case we want the override). local success success, title = pcall(new_title, val, match(decoded, "^.-%f[:]") or namespace) -- Otherwise, get the title with mw.title.makeTitle, which unconditionally adds the namespace prefix, but behaves like mw.title.new if the namespace is 0. if not success then title = make_title(namespace, decoded) end else error(format('Internal error: expected `prefix` for type `title` to be "force", "full override", "namespace override" or undefined, but saw %s', dump(prefix))) end local allow_external = param.allow_external if allow_external == true then return title or convert_val_error(val, name, "Wiktionary or external page title") elseif not allow_external then return title and is_internal_title(title) and title or convert_val_error(val, name, "Wiktionary page title") end error(format('Internal error: expected `allow_external` for type `title` to be of type "boolean" or undefined, but saw %s', dump(allow_external))) end, ["Wikimedia language"] = function(val, name, param) local fallback = param.fallback if fallback == true then return get_wm_lang_by_code_with_fallback(val) or convert_val_error(val, name, "Wikimedia language or language code") elseif not fallback then return get_wm_lang_by_code(val) or convert_val_error(val, name, "Wikimedia language code") end error(format('Internal error: expected `fallback` for type `Wikimedia language` to be of type "boolean" or undefined, but saw %s', dump(fallback))) end, }, { -- TODO: decode HTML entities in all input values. Non-trivial to implement, because we need to avoid any downstream functions decoding the output from this module, which would be double-decoding. Note that "title" has this implemented already, and it needs to have both the raw input and the decoded input to avoid double-decoding by me.title.new, so any implementation can't be as simple as decoding in __call then passing the result to the handler. __call = function(self, val, name, param, param_type, default) local val_type = type(val) -- TODO: check this for all possible parameter types. if val_type == param_type then return val elseif val_type ~= "string" then local expected = "string" if default and (param_type == "boolean" or param_type == "number") then expected = param_type .. " or " .. expected end error(format( "Internal error: %sargument %s has the type %s; expected a %s.", default and (default .. " for ") or "", name, dump(val_type), expected )) end local func = self[param_type] if func == nil then error(format("Internal error: %s is not a recognized parameter type.", dump(param_type))) end return func(val, name, param) end }) --[==[ func: export.convert_val(val, name, param) Convert a parameter value according to the associated specs listed in the `params` table passed to [[Module:parameters]]. `val` is the value to convert for a parameter whose name is `name` (used only in error messages). `param` is the spec (the value part of the `params` table for the parameter). In place of passing in the parameter name, `name` can be a function that throws an error, displaying the specified message along with the parameter name and value. This function processes all the conversion-related fields in `param`, including `type`, `set`, `sublist`, `convert`, etc. It returns the converted value. ]==] local function convert_val(val, name, param, default) local param_type = param.type or "string" -- If param.type is a function, resolve it to a recognized type. if is_callable(param_type) then param_type = param_type(val) end local convert, sublist = param.convert, param.sublist -- `val` might not be a string if it's the default value. if sublist and type(val) == "string" then local retlist, set = {}, param.set if convert then local thisindex, thisval, insval, parse_err = 0 if is_callable(name) then -- We assume the passed-in error function in `name` already shows the parameter name and raw value. function parse_err(msg) name(format("%s: item #%s=%s", msg_with_processed(msg, thisval, insval), thisindex, thisval) ) end else function parse_err(msg) error(format("%s: item #%s=%s of parameter %s=%s", msg_with_processed(msg, thisval, insval), thisindex, thisval, name, val) ) end end for v in split_sublist(val, name, sublist) do thisindex, thisval = thisindex + 1, v if set then v = check_set(v, name, param, param_type) end insert(retlist, convert(type_handlers(v, name, param, param_type, default), parse_err)) end else for v in split_sublist(val, name, sublist) do if set then v = check_set(v, name, param, param_type) end insert(retlist, type_handlers(v, name, param, param_type, default)) end end return retlist elseif param.set then val = check_set(val, name, param, param_type) end local retval = type_handlers(val, name, param, param_type, default) if convert then local parse_err if is_callable(name) then -- We assume the passed-in error function in `name` already shows the parameter name and raw value. if retval == val then -- This is an optimization to avoid creating a closure. The second arm works correctly even -- when retval == val. parse_err = name else function parse_err(msg) name(msg_with_processed(msg, val, retval)) end end else function parse_err(msg) error(format("%s: parameter %s=%s", msg_with_processed(msg, val, retval), name, val)) end end retval = convert(retval, parse_err) end -- If `sublist` is set but the input wasn't a string, return `retval` as a one-item list. if sublist then retval = {retval} end return retval end export.convert_val = convert_val -- used by [[Module:parameter utilities]] local function unknown_param(name, val, args_unknown) track("unknown parameters") args_unknown[name] = val return args_unknown end local function check_string_param_modifier(param_type, name, tag) if param_type and not (param_type == "string" or param_type == "parameter" or is_callable(param_type)) then internal_process_error( "%s cannot be set unless %s is set to %s (the default), %s or a function: parameter %s has the type %s.", tag, "type", "string", "parameter", name, param_type ) end end local function hole_error(params, name, listname, this, nxt, extra) -- `process_error` calls `dump` on values to be inserted into -- error messages, but with numeric lists this causes "numeric" -- to look like the name of the list rather than a description, -- as `dump` adds quote marks. Insert it early to avoid this, -- but add another %s specifier in all other cases, so that -- actual list names will be displayed properly. local offset, specifier, starting_from = 0, "%s", "" local msg = "Item %%d in the list of %s parameters must be given if item %%d is given, because %sthere shouldn't be any gaps due to missing%s parameters." local specs = {} if type(listname) == "string" then specs[2] = listname elseif type(name) == "number" then offset = name - 1 -- To get the original parameter. specifier = "numeric" -- If the list doesn't start at parameter 1, avoid implying -- there can't be any gaps in the numeric parameters if -- some parameter with a lower key is optional. for j = name - 1, 1, -1 do local _param = params[j] if not (_param and _param.required) then starting_from = format("(starting from parameter %d) ", dump(j + 1)) break end end else specs[2] = name end specs[1] = this + offset -- Absolute index for this item. insert(specs, nxt + offset) -- Absolute index for the next item. process_error(format(msg, specifier, starting_from, extra or ""), unpack(specs)) end local function check_disallow_holes(params, val, name, listname, extra) for i = 1, val.maxindex do if val[i] == nil then hole_error(params, name, listname, i, num_keys(val)[i], extra) end end end local function handle_holes(params, val, name) local param = params[name] local disallow_holes = param.disallow_holes -- Iterate up the list, and throw an error if a hole is found. if disallow_holes then check_disallow_holes(params, val, name, param.list, " or empty") end -- Iterate up the list, and throw an error if a hole is found due to a -- missing parameter, treating empty parameters as part of the list. This -- applies beyond maxindex if blank arguments are supplied beyond it, so -- isn't mutually exclusive with `disallow_holes`. local empty = val.empty if param.disallow_missing then if empty then -- Remove `empty` from `val`, so it doesn't get returned. val.empty = nil for i = 1, max(val.maxindex, empty.maxindex) do if val[i] == nil and not empty[i] then local keys = extend(num_keys(val), num_keys(empty)) sort(keys) hole_error(params, name, param.list, i, keys[i]) end end -- If there's no table of empty parameters, the check is identical to -- `disallow_holes`, except that the error message only refers to -- missing parameters, not missing or empty ones. If `disallow_holes` is -- also set, there's no point checking again. elseif not disallow_holes then check_disallow_holes(params, val, name, param.list) end end -- If `allow_holes` is set, there's nothing left to do. if param.allow_holes then -- do nothing -- Otherwise, remove any holes: `pairs` won't work, as it's unsorted, and -- iterating from 1 to `maxindex` times out with inputs like |100000000000=, -- so use num_keys to get a list of numerical keys sorted from lowest to -- highest, then iterate up the list, moving each value in `val` to the -- lowest unused positive integer key. This also avoids the need to create a -- new table. If `disallow_holes` is specified, then there can't be any -- holes in the list, so there's no reason to check again; this doesn't -- apply to `disallow_missing`, however. else if not disallow_holes then local keys, i = num_keys(val), 0 while true do i = i + 1 local key = keys[i] if key == nil then break elseif i ~= key then track("holes compressed") val[i], val[key] = val[key], nil end end end -- Some code depends on only numeric params being present when no holes are -- allowed (e.g. by checking for the presence of arguments using next()), so -- remove `maxindex`. val.maxindex = nil end end local function maybe_flatten(params, val, name) local param = params[name] if param.flatten then if param.allow_holes then process_error("For parameter %s, can't set both `allow_holes` and `flatten`", name) end if not param.sublist and param.type ~= "genders" and param.type ~= "labels" and param.type ~= "references" and param.type ~= "qualifier" and param.type ~= "form of tags" then process_error("For parameter %s, can only set `flatten` along with `sublist` or a list-generating type", name) end -- Do the flattening ourselves rather than calling flatten() in [[Module:table]], which will attempt to -- flatten non-list objects like title objects, and cause an error in the process. -- FIXME: We should do this in-place if possible. local newlist = {} for _, sublist in ipairs(val) do for _, item in ipairs(sublist) do insert(newlist, item) end end val = newlist end return val end -- If both `template_default` and `default` are given, `template_default` takes precedence, but only on the template or -- module page. This means a different default can be specified for the template or module page example. However, -- `template_default` doesn't apply if any args are set, which helps (somewhat) with examples on documentation pages -- transcluded into the template page. HACK: We still run into problems on documentation pages transcluded into the -- template page when pagename= is set. Check this on the assumption that pagename= is fairly standard. local function convert_default_val(name, param, pagename_set, any_args_set, add_empty_sublist) if not pagename_set then local val = param.template_default if val ~= nil and not any_args_set and is_own_page() then return convert_val(val, name, param, "template default") end end local val = param.default if val ~= nil then return convert_val(val, name, param, "default") -- Sublist parameters should return an empty table if not given, but only do -- this if the parameter isn't also a list (in which case it will already -- be an empty table). -- FIXME: do this once all modules that pass in a sublist parameter treat an empty sublist identically to a nil argument; some currently do things based on the fact an argument exists at all. -- elseif add_empty_sublist and param.sublist then --return {} end end --[==[ Process arguments with a given list of parameters. Return a table containing the processed arguments. The `args` parameter specifies the arguments to be processed; they are the arguments you might retrieve from {frame:getParent().args} (the template arguments) or in some cases {frame.args} (the invocation arguments). The `params` parameter specifies a list of valid parameters, and consists of a table. If an argument is encountered that is not in the parameter table, an error is thrown. The structure of the `params` table is as described above in the intro comment. '''WARNING:''' The `params` table is destructively modified to save memory. Nonetheless, different keys can share the same value objects in memory without causing problems. The `return_unknown` parameter, if set to {true}, prevents the function from triggering an error when it comes across an argument with a name that it doesn't recognise. Instead, the return value is a pair of values: the first is the processed arguments as usual, while the second contains all the unrecognised arguments that were left unprocessed. This allows you to do multi-stage processing, where the entire set of arguments that a template should accept is not known at once. For example, an inflection-table might do some generic processing on some arguments, but then defer processing of the remainder to the function that handles a specific inflectional type. ]==] function export.process(args, params, return_unknown) -- Process parameters for specific properties local args_new, args_unknown, any_args_set, required, patterns, list_args, index_list, args_placeholders, placeholders_n = {} -- TODO: memoize the processing of each unique `param` value, since it's common for the same value to be used for many parameter names. for name, param in pairs(params) do validate_name(name, "parameter names") if param ~= true then local spec_type = type(param) if type(param) ~= "table" then internal_process_error( "spec for parameter %s must be a table of specs or the value true, but found %s.", name, spec_type ~= "boolean" and spec_type or param ) end -- Populate required table, and make sure aliases aren't set to required. if param.required then if required == nil then required = {} end required[name] = true end local listname, alias_of = param.list, param.alias_of if alias_of then validate_name(alias_of, "the alias_of field of parameter ", name) if alias_of == name then internal_process_error( "parameter %s cannot be an alias of itself.", name ) end local main_param = params[alias_of] -- Check that the alias_of is set to a valid parameter. if not (main_param == true or type(main_param) == "table") then internal_process_error( "parameter %s is an alias of an invalid parameter.", name ) end validate_alias_options(param, name, main_param, alias_of) -- Aliases can't be lists unless the canonical parameter is also a list. if listname and (main_param == true or not main_param.list) then internal_process_error( "list parameter %s is set as an alias of %s, which is not a list parameter.", name, alias_of ) -- Can't be an alias of an alias. elseif main_param ~= true then local main_alias_of = main_param.alias_of if main_alias_of ~= nil then internal_process_error( "alias_of cannot be set to another alias: parameter %s is set as an alias of %s, which is in turn an alias of %s. Set alias_of for %s to %s.", name, alias_of, main_alias_of, name, main_alias_of ) end end end local replaced_by = param.replaced_by if replaced_by then -- replaced_by can be `false`, which is OK validate_name(replaced_by, "the replaced_by field of parameter ", name) if replaced_by == name then internal_process_error( "parameter %s cannot be replaced by itself.", name ) end local main_param = params[replaced_by] -- Check that the replaced_by is set to a valid parameter. if not (main_param == true or type(main_param) == "table") then internal_process_error( "parameter %s is set to be replaced by an invalid parameter.", name ) end -- Can't be a replaced-by of a replaced-by. if main_param ~= true then local main_replaced_by = main_param.replaced_by if main_replaced_by ~= nil then internal_process_error( "replaced_by cannot be set to another replaced-by parameter: parameter %s is set as replaced by %s, which is in turn replaced by %s. Set replaced_by for %s to %s.", name, replaced_by, main_replaced_by, name, main_replaced_by ) end end if param.instead ~= nil then internal_process_error("the `instead` tag can only be given when `replaced_by` is set to `false`.") end elseif replaced_by == false then if param.instead ~= nil and type(param.instead) ~= "string" then internal_process_error( "the `instead` tag must be a string, but saw %s.", param.instead ) end end if replaced_by ~= nil then if param.reason ~= nil and type(param.reason) ~= "string" then internal_process_error( "the `reason` tag must be a string, but saw %s.", param.reason ) end end if listname then if not alias_of then local key = name if type(name) == "string" then key = gsub(name, "\1", "") end local list_arg = {maxindex = 0} args_new[key] = list_arg if list_args == nil then list_args = {} end list_args[key] = list_arg end local list_type = type(listname) if list_type == "string" then -- If the list property is a string, then it represents the name -- to be used as the prefix for list items. This is for use with lists -- where the first item is a numbered parameter and the -- subsequent ones are named, such as 1, pl2, pl3. patterns = save_pattern(name, listname, patterns or {}) elseif listname ~= true then internal_process_error( "list field for parameter %s must be a boolean, string or undefined, but saw a %s.", name, list_type ) elseif type(name) == "number" then if index_list ~= nil then internal_process_error( "only one numeric parameter can be a list, unless the list property is a string." ) end -- If the name is a number, then all indexed parameters from -- this number onwards go in the list. index_list = name else patterns = save_pattern(name, name, patterns or {}) end if find(name, "\1", nil, true) then if args_placeholders then placeholders_n = placeholders_n + 1 args_placeholders[placeholders_n] = name else args_placeholders, placeholders_n = {name}, 1 end end end end end --Process required changes to `params`. if args_placeholders then for i = 1, placeholders_n do local name = args_placeholders[i] params[gsub(name, "\1", "")], params[name] = params[name], nil end end -- Process the arguments for name, val in pairs(args) do any_args_set = true validate_name(name, "argument names", nil, true) -- Guaranteeing that all values are strings avoids issues with type coercion being inconsistent between functions. local val_type = type(val) if val_type ~= "string" then internal_process_error( "argument %s has the type %s; all arguments must be strings.", name, val_type ) end local orig_name, raw_type, index, canonical = name, type(name) if raw_type == "number" then if index_list and name >= index_list then index = name - index_list + 1 name = index_list end elseif patterns then -- Does this argument name match a pattern? for pattern, pname in next, patterns do index = match(name, pattern) -- It matches, so store the parameter name and the -- numeric index extracted from the argument name. if index then index = tonumber(index) name = pname break end end end local param = params[name] -- If the argument is not in the list of parameters, store it in a separate list. if not param then args_unknown = unknown_param(name, val, args_unknown or {}) elseif param == true then canonical = orig_name val = php_trim(val) if val ~= "" then -- If the parameter is duplicated, throw an error. if args_new[name] ~= nil then process_error( "Parameter %s has been entered more than once. This is probably because a parameter alias has been used.", canonical ) end args_new[name] = val end else if param.replaced_by == false then process_error( ("Parameter %%s has been removed and is no longer valid%s.%s"):format( param.reason and ", " .. param.reason or "", param.instead and " Instead, " .. param.instead .. "." or ""), name ) elseif param.replaced_by then process_error( ("Parameter %%s has been replaced by %%s%s."):format( param.reason and ", " .. param.reason or ""), name, param.replaced_by ) end if param.deprecated then track("deprecated parameter", name) end if param.require_index then -- Disallow require_index for numeric parameter names, as this doesn't make sense. if raw_type == "number" then internal_process_error( "cannot set require_index for numeric parameter %s.", name ) -- If a parameter without the trailing index was found, and -- require_index is set on the param, treat it -- as if it isn't recognized. elseif not index then args_unknown = unknown_param(name, val, args_unknown or {}) end end -- Check that separate_no_index is not being used with a numeric parameter. if param.separate_no_index then if raw_type == "number" then internal_process_error( "cannot set separate_no_index for numeric parameter %s.", name ) elseif type(param.alias_of) == "number" then internal_process_error( "cannot set separate_no_index for parameter %s, as it is an alias of numeric parameter %s.", name, param.alias_of ) end end -- If no index was found, use 1 as the default index. -- This makes list parameters like g, g2, g3 put g at index 1. -- If `separate_no_index` is set, then use 0 as the default instead. if not index and param.list then index = param.separate_no_index and 0 or 1 end -- Normalize to the canonical parameter name. If it's a list, but the alias is not, then determine the index. local raw_name = param.alias_of if raw_name then raw_type = type(raw_name) if raw_type == "number" then name = raw_name local main_param = params[raw_name] if main_param ~= true and main_param.list then if not index then index = param.separate_no_index and 0 or 1 end canonical = raw_name + index - 1 else canonical = raw_name end else name = gsub(raw_name, "\1", "") local main_param = params[name] if not index and main_param ~= true and main_param.list then index = param.separate_no_index and 0 or 1 end if not index or index == 0 then canonical = name elseif name == raw_name then canonical = name .. index else canonical = gsub(raw_name, "\1", index) end end else canonical = orig_name end -- Only recognize demo parameters if this is the current template or module's -- page, or its documentation page. if param.demo and not is_own_page("include_documentation") then args_unknown = unknown_param(name, val, args_unknown or {}) end -- Remove leading and trailing whitespace unless no_trim is true. if param.no_trim then check_string_param_modifier(param.type, name, "no_trim") else val = php_trim(val) end -- Empty string is equivalent to nil unless allow_empty is true. if param.allow_empty then check_string_param_modifier(param.type, name, "allow_empty") elseif val == "" then -- If `disallow_missing` is set, keep track of empty parameters -- via the `empty` field in `arg`, which will be used by the -- `disallow_missing` check. This will be deleted before -- returning. if index and param.disallow_missing then local arg = args_new[name] local empty = arg.empty if empty == nil then empty = {maxindex = 0} arg.empty = empty end empty[index] = true if index > empty.maxindex then empty.maxindex = index end end val = nil end -- Allow boolean false. if val ~= nil then -- Convert to proper type if necessary. local main_param = params[raw_name] if main_param ~= true then val = convert_val(val, orig_name, main_param or param) end -- Mark it as no longer required, as it is present. if required then required[name] = nil end -- Store the argument value. if index then local arg = args_new[name] -- If the parameter is duplicated, throw an error. if arg[index] ~= nil then process_error( "Parameter %s has been entered more than once. This is probably because a list parameter has been entered without an index and with index 1 at the same time, or because a parameter alias has been used.", canonical ) end arg[index] = val -- Store the highest index we find. local maxindex = arg.maxindex if index > maxindex then maxindex = index end if arg[0] ~= nil then arg.default, arg[0] = arg[0], nil if maxindex < 1 then maxindex = 1 end end arg.maxindex = maxindex if not params[name].list then args_new[name] = val -- Don't store index 0, as it's a proxy for the default. elseif index > 0 then arg[index] = val end else -- If the parameter is duplicated, throw an error. if args_new[name] ~= nil then process_error( "Parameter %s has been entered more than once. This is probably because a parameter alias has been used.", canonical ) end if not raw_name then args_new[name] = val else local main_param = params[raw_name] if main_param ~= true and main_param.list then local main_arg = args_new[raw_name] main_arg[1] = val -- Store the highest index we find. if main_arg.maxindex < 1 then main_arg.maxindex = 1 end else args_new[raw_name] = val end end end end end end -- Remove holes in any list parameters if needed. This must be handled -- straight after the previous loop, as any instances of `empty` need to be -- converted to nil. if list_args then for name, val in next, list_args do handle_holes(params, val, name) end end -- If the current page is the template which invoked this Lua instance, then ignore the `require` flag, as it -- means we're viewing the template directly. Required parameters sometimes have a `template_default` key set, -- which gets used in such cases as a demo. -- Note: this won't work on other pages in the Template: namespace (including the /documentation subpage), -- or if the #invoke: is on a page in another namespace. local pagename_set = args_new.pagename -- Handle defaults. for name, param in pairs(params) do if param ~= true then local arg_new = args_new[name] if arg_new == nil then args_new[name] = convert_default_val(name, param, pagename_set, any_args_set, true) elseif param.list and arg_new[1] == nil then local default_val = convert_default_val(name, param, pagename_set, any_args_set) if default_val ~= nil then arg_new[1] = default_val if arg_new.maxindex == 0 then arg_new.maxindex = 1 end end end end end -- Flatten nested lists if called for. This must come after setting the default. if list_args then for name, val in next, list_args do args_new[name] = maybe_flatten(params, val, name) end end -- The required table should now be empty. -- If any parameters remain, throw an error, unless we're on the current template or module's page. if required and next(required) ~= nil and not is_own_page() then params_list_error(required, "required") -- Return the arguments table. -- If there are any unknown parameters, throw an error, unless return_unknown is set, in which case return args_unknown as a second return value. elseif return_unknown then return args_new, args_unknown or {} elseif args_unknown and next(args_unknown) ~= nil then params_list_error(args_unknown, "not used by this template") end return args_new end return export c23kzesggrlowmn06oii7zu1bbzht4t Mòideal:table 828 9202 86125 75273 2026-07-16T23:36:50Z Altronic 4137 Update module to the most recent version on English Wiktionary 86125 Scribunto text/plain local export = {} --[==[ intro: This module provides functions for dealing with Lua tables. All of them, except for two helper functions, take a table as their first argument. Some functions are available as methods in the arrays created by [[Module:array]]. Functions by what they do: * Create a new table: ** `shallowCopy`, `deepCopy`, `removeDuplicates`, `numKeys`, `compressSparseArray`, `keysToList`, `reverse`, `invert`, `listToSet` * Create an array: ** `removeDuplicates`, `numKeys`, `compressSparseArray`, `keysToList`, `reverse` * Return information about the table: ** `size`, `length`, `contains`, `isArray`, `deepEquals` * Treat the table as an array (that is, operate on the values in the array portion of the table: values indexed by consecutive integers starting at {1}): ** `removeDuplicates`, `length`, `contains`, `serialCommaJoin`, `reverseIpairs`, `reverse`, `invert`, `listToSet`, `isArray` * Treat a table as a sparse array (that is, operate on values indexed by non-consecutive integers): ** `numKeys`, `maxIndex`, `compressSparseArray`, `sparseConcat`, `sparseIpairs` * Generate an iterator: ** `sparseIpairs`, `sortedPairs`, `reverseIpairs` * Other functions: ** `sparseConcat`, `serialCommaJoin`, `reverseConcat` The original version was a copy of {{w|Module:TableTools}} on Wikipedia via [[c:Module:TableTools|Module:TableTools]] on Commons, but in the course of time this module has been almost completely rewritten, with many new functions added. The main legacy of this is the use of camelCase for function names rather than snake_case, as is normal in the English Wiktionary. ]==] local load_module = "Module:load" local math_module = "Module:math" local table = table local concat = table.concat local dump = mw.dumpObject local ipairs = ipairs local ipairs_default_iter = ipairs{export} local next = next local pairs = pairs local require = require local select = select local signed_index -- defined as export.signedIndex local table_len -- defined as export.length local type = type --[==[ 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 is_integer(...) is_integer = require(math_module).is_integer return is_integer(...) end local function safe_require(...) safe_require = require(load_module).safe_require return safe_require(...) end --[==[ Given an array and a signed index, returns the true table index. If the signed index is negative, the array will be counted from the end, where {-1} is the highest index in the array; otherwise, the returned index will be the same. To aid optimization, the first argument may be a number representing the array length instead of the array itself; this is useful when the array length is already known, as it avoids recalculating it each time this function is called.]==] function export.signedIndex(t, k) if not is_integer(k) then error("index must be an integer") end return k < 0 and (type(t) == "table" and table_len(t) or t) + k + 1 or k end signed_index = export.signedIndex --[==[ An iterator which works like `pairs`, but ignores any `__pairs` metamethod.]==] function export.rawPairs(t) return next, t, nil end --[==[ An iterator which works like `ipairs`, but ignores any `__ipairs` metamethod.]==] function export.rawIpairs(t) return ipairs_default_iter, t, 0 end --[==[ This returns the length of a table, or the first integer key n counting from 1 such that t[n + 1] is nil. It is a more reliable form of the operator `#`, which can become unpredictable under certain circumstances due to the implementation of tables under the hood in Lua, and therefore should not be used when dealing with arbitrary tables. `#` also does not use metamethods, so will return the wrong value in cases where it is desirable to take these into account (e.g. data loaded via `mw.loadData`). If `raw` is set, then metamethods will be ignored, giving the true table length. For arrays, this function is faster than `export.size`.]==] function export.length(t, raw) local n = 0 if raw then for i in ipairs_default_iter, t, 0 do n = i end return n end repeat n = n + 1 until t[n] == nil return n - 1 end table_len = export.length local function getIteratorValues(i, j , step, t_len) i, j = i and signed_index(t_len, i), j and signed_index(t_len, j) if step == nil then i, j = i or 1, j or t_len return i, j, j < i and -1 or 1 elseif step == 0 or not is_integer(step) then error("step must be a non-zero integer") elseif step < 0 then return i or t_len, j or 1, step end return i or 1, j or t_len, step end --[==[ Given an array `list` and function `func`, iterate through the array applying {func(r, k, v)}, and returning the result, where `r` is the value calculated so far, `k` is an index, and `v` is the value at index `k`. For example, {reduce(array, function(a, _, v) return a + v end)} will return the sum of `array`. Optional arguments: * `i`: start index; negative values count from the end of the array * `j`: end index; negative values count from the end of the array * `step`: step increment These must be non-zero integers. The function will determine where to iterate from, whether to iterate forwards or backwards and by how much, based on these inputs (see examples below for default behaviours). Examples: # No values for i, j or step results in forward iteration from the start to the end in steps of 1 (the default). # step=-1 results in backward iteration from the end to the start in steps of 1. # i=7, j=3 results in backward iteration from indices 7 to 3 in steps of 1 (i.e. step=-1). # j=-3 results in forward iteration from the start to the 3rd last index. # j=-3, step=-1 results in backward iteration from the end to the 3rd last index.]==] function export.reduce(t, func, i, j, step) i, j, step = getIteratorValues(i, j, step, table_len(t)) local ret = t[i] for k = i + step, j, step do ret = func(ret, k, t[k]) end return ret end do local function replace(t, func, i, j, step, generate) local t_len = table_len(t) -- Normalized i, j and step, based on the inputs. local norm_i, norm_j, norm_step = getIteratorValues(i, j, step, t_len) if norm_step > 0 then i, j, step = 1, t_len, 1 else i, j, step = t_len, 1, -1 end -- "Signed" variables are multiplied by -1 if `step` is negative. local t_new, signed_i, signed_j = generate and {} or t, norm_i * step, norm_j * step for k = i, j, step do -- Replace the values iff they're within the i to j range and `step` wouldn't skip the key. -- Note: i > j if `step` is positive; i < j if `step` is negative. Otherwise, the range is empty. local signed_k = k * step if signed_k >= signed_i and signed_k <= signed_j and (k - norm_i) % norm_step == 0 then t_new[k] = func(k, t[k]) -- Otherwise, add the existing value if `generate` is set. elseif generate then t_new[k] = t[k] end end return t_new end --[==[ Given an array `list` and function `func`, iterate through the array applying {func(k, v)} (where `k` is an index, and `v` is the value at index `k`), replacing the relevant values with the result. For example, {apply(array, function(_, v) return 2 * v end)} will double each member of the array. Optional arguments: * `i`: start index; negative values count from the end of the array * `j`: end index; negative values count from the end of the array * `step`: step increment These must be non-zero integers. The function will determine where to iterate from, whether to iterate forwards or backwards and by how much, based on these inputs (see examples below for default behaviours). Examples: # No values for i, j or step results in forward iteration from the start to the end in steps of 1 (the default). # step=-1 results in backward iteration from the end to the start in steps of 1. # i=7, j=3 results in backward iteration from indices 7 to 3 in steps of 1 (i.e. step=-1). # j=-3 results in forward iteration from the start to the 3rd last index. # j=-3, step=-1 results in backward iteration from the end to the 3rd last index.]==] function export.apply(t, func, i, j, step) return replace(t, func, i, j, step, false) end --[==[ Given an array `list` and function `func`, iterate through the array applying {func(k, v)} (where `k` is an index, and `v` is the value at index `k`), and return a shallow copy of the original array with the relevant values replaced. For example, {generate(array, function(_, v) return 2 * v end)} will return a new array in which each value has been doubled. Optional arguments: * `i`: start index; negative values count from the end of the array * `j`: end index; negative values count from the end of the array * `step`: step increment These must be non-zero integers. The function will determine where to iterate from, whether to iterate forwards or backwards and by how much, based on these inputs (see examples below for default behaviours). Examples: # No values for i, j or step results in forward iteration from the start to the end in steps of 1 (the default). # step=-1 results in backward iteration from the end to the start in steps of 1. # i=7, j=3 results in backward iteration from indices 7 to 3 in steps of 1 (i.e. step=-1). # j=-3 results in forward iteration from the start to the 3rd last index. # j=-3, step=-1 results in backward iteration from the end to the 3rd last index.]==] function export.generate(t, func, i, j, step) return replace(t, func, i, j, step, true) end end --[==[ Given an array `list` and function `func`, iterate through the array applying {func(k, v)} (where `k` is an index, and `v` is the value at index `k`), and returning whether the function is true for all iterations. Optional arguments: * `i`: start index; negative values count from the end of the array * `j`: end index; negative values count from the end of the array * `step`: step increment These must be non-zero integers. The function will determine where to iterate from, whether to iterate forwards or backwards and by how much, based on these inputs (see examples below for default behaviours). Examples: # No values for i, j or step results in forward iteration from the start to the end in steps of 1 (the default). # step=-1 results in backward iteration from the end to the start in steps of 1. # i=7, j=3 results in backward iteration from indices 7 to 3 in steps of 1 (i.e. step=-1). # j=-3 results in forward iteration from the start to the 3rd last index. # j=-3, step=-1 results in backward iteration from the end to the 3rd last index.]==] function export.all(t, func, i, j, step) i, j, step = getIteratorValues(i, j, step, table_len(t)) for k = i, j, step do if not func(k, t[k]) then return false end end return true end --[==[ Given an array `list` and function `func`, iterate through the array applying {func(k, v)} (where `k` is an index, and `v` is the value at index `k`), and returning whether the function is true for at least one iteration. Optional arguments: * `i`: start index; negative values count from the end of the array * `j`: end index; negative values count from the end of the array * `step`: step increment These must be non-zero integers. The function will determine where to iterate from, whether to iterate forwards or backwards and by how much, based on these inputs (see examples below for default behaviours). Examples: # No values for i, j or step results in forward iteration from the start to the end in steps of 1 (the default). # step=-1 results in backward iteration from the end to the start in steps of 1. # i=7, j=3 results in backward iteration from indices 7 to 3 in steps of 1 (i.e. step=-1). # j=-3 results in forward iteration from the start to the 3rd last index. # j=-3, step=-1 results in backward iteration from the end to the 3rd last index.]==] function export.any(t, func, i, j, step) i, j, step = getIteratorValues(i, j, step, table_len(t)) for k = i, j, step do if not not (func(k, t[k])) then return true end end return false end --[==[ Joins an array with serial comma and serial conjunction, normally {"and"}. An improvement on {mw.text.listToText}, which doesn't properly handle serial commas. Options: * `conj`: Conjunction to use; defaults to {"and"}. * `punc`: Punctuation to use; default to {","}. * `dontTag`: Don't tag the serial comma and serial {"and"}. For error messages, in which HTML cannot be used. * `dump`: Each item will be serialized with {mw.dumpObject}. For warnings and error messages.]==] function export.serialCommaJoin(seq, options) -- If the `dump` option is set, determine the table length as part of the -- dump loop, instead of calling `table_len` separately. local length if options and options.dump then local i, item = 1, seq[1] if item ~= nil then local dumped = {} repeat dumped[i] = dump(item) i = i + 1 item = seq[i] until item == nil seq = dumped end length = i - 1 else length = table_len(seq) end if length == 0 then return "" elseif length == 1 then return seq[1] end local conj = options and options.conj if conj == nil then conj = "and" end if length == 2 then return seq[1] .. " " .. conj .. " " .. seq[2] end local punc, dont_tag if options then punc = options.punc if punc == nil then punc = "," end dont_tag = options.dontTag else punc = "," end local comma if dont_tag then comma = "" -- since by default the serial comma doesn't display, when we can't tag we shouldn't display it. conj = " " .. conj .. " " else comma = "<span class=\"serial-comma\">" .. punc .. "</span>" conj = "<span class=\"serial-and\"> " .. conj .. "</span> " end return concat(seq, punc .. " ", 1, length - 1) .. comma .. conj .. seq[length] end --[==[ A function which works like `table.concat`, but respects any `__index` metamethod. This is useful for data loaded via `mw.loadData`.]==] function export.concat(t, sep, i, j) local list, k = {}, 0 while true do k = k + 1 local v = t[k] if v == nil then return concat(list, sep, i, j) end list[k] = v end end --[==[ Add a list of aliases for a given key to a table. The aliases must be given as a table.]==] function export.alias(t, k, aliases) for _, alias in pairs(aliases) do t[alias] = t[k] end end local mt = {} function mt:__index(k) local submodule = safe_require("Module:table/" .. k) self[k] = submodule return submodule end return setmetatable(export, mt) exw27st65r96fvgc4krew9cy4eyvdow Teamplaid:inflecion-table-top 10 16618 86100 2026-07-16T20:34:13Z Altronic 4137 Copy from English Wiktionary 86100 wikitext text/x-wiki <includeonly><!-- special wrapper to prevent substitution of this template -->{{ safesubst:<noinclude/>#ifeq:{{ safesubst:<noinclude/>NAMESPACE}}|{{NAMESPACE}}| <div class="inflection-table-wrapper inflection-table-{{{palette|grey}}} {{#ifeq:{{{title}}}|-|inflection-table-no-title}} {{#if:{{{tall|}}}|inflection-table-collapsible inflection-table-collapsed no-vc}} {{{class|}}}" style="width: fit-content{{#if:{{{min-width|}}}|; min-width: {{{min-width}}}}}" data-toggle-category="{{{category|inflection}}}"><templatestyles src="Template:inflection-table-top/style.css" /> {{{!}} class="inflection-table {{#if:{{{lang|}}}|inflection-table-{{{lang}}}}} {{#if:{{{vs-category|}}}|vsSwitcher}}" {{#if:{{{vs-category|}}}|data-toggle-category="{{{vs-category}}}"}} <!-- {{!}} is used in place of | due to the special safesubst: wrapper --> {{#ifeq:{{{title}}}|-|| {{!}}+ class="inflection-table-title {{#if:{{{vsToggleElement|}}}|vsToggleElement}}" {{!}} {{{title|Inflection of ''{{PAGENAME}}''}}} {{!}}- }} |{{color||Do not substitute this template}}}} </includeonly><noinclude>{{documentation}}</noinclude> l8rs36ua7oyz8o9uy60ih065hfihfq3 Mòideal:gd-conj 828 16619 86101 2026-07-16T20:41:38Z Altronic 4137 Copy from English Wiktionary 86101 Scribunto text/plain local export = {} local wikitext = [=[ ! rowspan="2" | indicative ! colspan="2" | independent ! colspan="2" | dependent |- class="secondary" ! class="secondary" | [[personal]] ! class="secondary" | [[impersonal]] ! class="secondary" | [[personal]] ! class="secondary" | [[impersonal]] |- ! past | {{{past-indep}}} | {{{past-indep-impersonal}}} | {{{past-dep}}} | {{{past-dep-impersonal}}} |- ! future | {{{fut-indep}}} | {{{fut-indep-impersonal1}}}</br>{{{fut-indep-impersonal2}}} | {{{fut-dep}}} | {{{fut-dep-impersonal1}}}</br>{{{fut-indep-impersonal2}}} |- ! relative future | {{{relfut}}} | {{{relfut-impersonal}}} | — | — |- | class="separator" colspan=9 | |- class="inflection-table-cyan" ! rowspan="2" | conditional ! colspan="2" | independent ! colspan="2" | dependent |- class="inflection-table-cyan" ! class="secondary" | [[personal]] ! class="secondary" | [[impersonal]] ! class="secondary" | [[personal]] ! class="secondary" | [[impersonal]] |- class="inflection-table-cyan" ! first person singular | {{{cond-indep-first}}} | rowspan=3 | {{{cond-indep-first-impersonal}}}<br>{{{cond-indep-first-impersonal-l1}}}<sup>1</sup><br>{{{cond-indep-first-impersonal-l2}}}<sup>1</sup> | {{{cond-dep-first}}} | rowspan=3 | {{{cond-dep-first-impersonal}}}<br>{{{cond-dep-first-impersonal-l1}}}<sup>1</sup><br>{{{cond-dep-first-impersonal-l2}}}<sup>1</sup> |- class="inflection-table-cyan" ! first person plural | {{{cond-indep-first-pl}}}<br>{{{cond-indep-third}}} sinn | {{{cond-dep-first-pl}}}<br>{{{cond-dep-third}}} sinn |- class="inflection-table-cyan" ! second/third person | {{{cond-indep-third}}} | {{{cond-dep-third}}} |- | class="separator" colspan=4 | ! colspan=5 rowspan=6 class=blank-end-row | |- class="inflection-table-green" ! rowspan=2 | imperative ! colspan=3 | independent |- class="inflection-table-green" ! class="secondary" | singular ! class="secondary" | plural ! class="secondary" | [[impersonal]] |- class="inflection-table-green" ! first person | {{{imper-first}}} | {{{imper-first-pl}}} | rowspan=3 | {{{imper-impersonal1}}}<br>{{{imper-impersonal2}}} |- class="inflection-table-green" ! second person | {{{imper-second}}} | {{{imper-second-pl}}} |- class="inflection-table-green" ! third person | colspan=2 | {{{imper-third}}} |- | class="separator" colspan=4 | ! colspan=5 rowspan=4 class=blank-end-row | |- class="inflection-table-amber" ! past participle | colspan=3 | {{{participle}}} |- | class="separator" colspan=4 | |- class="inflection-table-yellow" ! verbal noun | colspan=3 | {{{vn}}} ]=] local participle = nil local vn = nil local fut = nil local langcode = require("Module:languages").getByCode("gd") local m_links = require("Module:links") local function mutate(term) local lower = mw.ustring.lower(term) local out = term if mw.ustring.find(lower, "^[bsdfgmpst]h") then return out end if mw.ustring.find(lower, "^[bcdfgmpt]") then out = mw.ustring.gsub(term, "^(.)", "%1h") elseif mw.ustring.find(lower, "^s[aeiouàèìòùáéólnr]") then out = mw.ustring.gsub(term, "^(.)", "%1h") end if mw.ustring.find(lower, "^[k]") then out = mw.ustring.gsub(term, "^(.)", "ch") end if mw.ustring.find(lower, "^[aàeèiìoòuùáéó]") then out = mw.ustring.gsub(term, "^(.)", "dh'%1") elseif mw.ustring.find(out, "^fh[aeiouàèìòùáéó]") then out = mw.ustring.gsub(out, "^(.)", "dh'%1") end return out end local function ending(type, stem, broad, slender) local out if type == "broad" then out = stem .. broad else out = stem .. slender end return out end local function get_vowel_type(string) local function contains(table, entry) for _, value in ipairs(table) do if value == entry then return true end end return false end local len = mw.ustring.len(string) local broad = { 'a', 'à', 'á', 'o', 'ò', 'ó'} local slender = { 'e', 'è', 'é', 'i', 'ì' } for i = len, 1, -1 do local char = mw.ustring.sub(string, i, i) if contains(broad, char) then return "broad" elseif contains(slender, char) then return "slender" end end return "no-vowel" end local function conjugate(param, term) local conj = term local type = get_vowel_type(conj) if param == "participle" then if participle then return m_links.full_link({ lang = langcode, term = participle }, nil, true) else return m_links.full_link({ lang = langcode, term = ending(type, conj, "ta", "te") }) end end if param == "vn" then conj = m_links.full_link({ lang = langcode, term = vn }, nil, true) return conj end -- switch statement of all the possible conjugation types -- indicative if param == "past-indep" or param == "past-dep" then conj = mutate(conj) elseif param == "past-indep-impersonal" or param == "past-dep-impersonal" then conj = mutate(conj) conj = ending(type, conj, "adh", "eadh") elseif param == "fut-indep" then if fut ~= nil then conj = fut type = get_vowel_type(conj) end conj = ending(type, conj, "aidh", "idh") elseif param == "fut-indep-impersonal1" or param == "fut-dep-impersonal1" or param == "imper-impersonal1" then if fut ~= nil then conj = fut type = get_vowel_type(conj) end conj = ending(type, conj, "ar", "ear") elseif param == "fut-indep-impersonal2" or param == "fut-dep-impersonal2" or param == "imper-impersonal2" then if fut ~= nil then -- TODO: forms with syncope don't have the ~tar forms, see [[labhair]] as an example conj = fut type = get_vowel_type(conj) end conj = ending(type, conj, "tar", "tear") elseif param == "fut-dep" then return m_links.full_link({ term = conj, lang = langcode }, nil, true) elseif param == "relfut" then conj = mutate(conj) conj = ending(type, conj, "as", "eas") elseif param == "relfut-impersonal" then conj = mutate(conj) conj = ending(type, conj, "ar", "ear") -- conditional elseif param == "cond-indep-first" then conj = mutate(conj) conj = ending(type, conj, "ainn", "inn") elseif param == "cond-indep-first-impersonal" then conj = mutate(conj) conj = ending(type, conj, "tadh", "teadh") elseif param == "cond-indep-first-impersonal-l1" then conj = mutate(conj) conj = ending(type, conj, "aist", "ist") elseif param == "cond-indep-first-impersonal-l2" then conj = mutate(conj) conj = ending(type, conj, "aiste", "iste") elseif param == "cond-indep-first-pl" then conj = mutate(conj) conj = ending(type, conj, "amaid", "eamaid") elseif param == "cond-indep-third" then conj = mutate(conj) conj = ending(type, conj, "adh", "eadh") elseif param == "cond-dep-first" then conj = ending(type, conj, "ainn", "inn") elseif param == "cond-dep-first-impersonal" then conj = ending(type, conj, "tadh", "teadh") elseif param == "cond-dep-first-impersonal-l1" then conj = ending(type, conj, "aist", "ist") elseif param == "cond-dep-first-impersonal-l2" then conj = ending(type, conj, "aiste", "iste") elseif param == "cond-dep-first-pl" then conj = ending(type, conj, "amaid", "eamaid") elseif param == "cond-dep-third" then conj = ending(type, conj, "adh", "eadh") elseif param == "imper-first" then conj = ending(type, conj, "am", "eam") elseif param == "imper-first-pl" then conj = ending(type, conj, "amaid", "eamaid") elseif param == "imper-second" then return m_links.full_link({ term = conj, lang = langcode }, nil, true) elseif param == "imper-second-pl" then conj = ending(type, conj, "aibh", "ibh") elseif param == "imper-third" then conj = ending(type, conj, "adh", "eadh") end return m_links.full_link({ term = conj, lang = langcode }, nil, true) end function export.draw(frame) local parentargs = frame:getParent().args local params = { [1] = { required = true, type = "parameter" }, ["pp"] = true, ["fut"] = true } local args = require("Module:parameters").process(parentargs, params) local term = require("Module:headword/data").pagename vn = args[1] if args["pp"] ~= nil then participle = args["pp"] end if args["fut"] ~= nil then fut = args["fut"] end local result = frame:expandTemplate{ title = "inflection-table-top", args = { title = "Conjugation of " .. m_links.full_link({ lang = langcode, alt = term }, 'term') .. " ([[Appendix:Scottish Gaelic verbs|regular]])", tall = "yes", palette = "blue" } } result = result .. '\n' result = result .. mw.ustring.gsub(wikitext, "{{{([a-z0-9_-]+)}}}", function(param) return conjugate(param, term) end) result = result .. frame:expandTemplate{ title = "inflection-table-bottom", args = { notes = "1. Lewis dialect form" } } return result end return export il10o3zqej7xhz0unt54w4p0qs77bsv Teamplaid:gd-conj 10 16620 86102 2026-07-16T20:59:50Z Altronic 4137 Copy from English Wiktionary 86102 wikitext text/x-wiki {{#invoke:gd-conj|draw|{{{1}}}|pp={{{pp}}}|fut={{{fut}}}}}<noinclude>{{documentation}}</noinclude> srg0ylb933ecar1yxblrd3d609mfi0z Mòideal:parameters/data 828 16621 86104 2026-07-16T21:03:41Z Altronic 4137 Copy from English Wiktionary 86104 Scribunto text/plain local list_to_set = require("Module:table").listToSet local alias_of_2 = {alias_of = 2} local boolean = {type = "boolean"} local empty_list = {} local list = {list = true} local list_allow_holes_separate_no_index = {list = true, allow_holes = true, separate_no_index = true} local required = {required = true} local required_default_ = {required = true, default = ""} local required_lang_default_und = {required = true, type = "language", default = "und"} local type_labels = {type = "labels"} local type_qualifier = {type = "qualifier"} local type_references = {type = "references"} local m = {} -- [[Module:anchors]] m["anchor"] = { [1] = {required = true, list = true, disallow_holes = true}, } m["senseid"] = { [1] = required_lang_default_und, [2] = required_default_, id = alias_of_2, tag = {set = list_to_set{"li", "p"}, default = "li"}, } m["etymid"] = { [1] = required_lang_default_und, [2] = required_default_, id = alias_of_2, } -- [[Module:etymon]] m["etymon"] = { [1] = required_lang_default_und, [2] = {list = true, disallow_holes = true}, id = true, title = true, tree = boolean, text = true, exnihilo = boolean, etydate = true, doublet = {sublist = "comma without whitespace"}, rfe = true, etystub = true, nl = boolean, pos = true, notree = {default = false, type = "boolean"}, nocat = {default = false, type = "boolean"}, nodot = {default = false, type = "boolean"}, dot = true, json = {default = false, type = "boolean"}, } -- [[Module:transclude]] m["transclude"] = { [1] = {required = true, type = "language"}, [2] = {list = true, required = true}, id = true, sort = true, nogloss = {default = false, type = "boolean"}, no_truncate_gloss = boolean, include_place_extra_info = boolean, place_translation_follows = boolean, place_addl = true, lb = true, nolb = true, nocat = boolean, to = boolean, t = list, indent = true, dot = boolean, pagename = true, } -- [[Module:translations]] m["translation"] = { [1] = required_lang_default_und, [2] = true, [3] = list, alt = true, id = true, sc = {type = "script"}, tr = true, ts = true, lit = true, l = type_labels, ll = type_labels, q = type_qualifier, qq = type_qualifier, ref = type_references, } m["t-needed"] = { [1] = required_lang_default_und, [2] = {set = list_to_set{"usex", "quote"}}, nocat = boolean, sort = true, } m["trans-top"] = { [1] = true, id = true, ["column-width"] = true, } m["trans-top-also"] = { [1] = required, [2] = list, id = list_allow_holes_separate_no_index, ["column-width"] = true, } m["checktrans-top"] = { [1] = true, ["column-width"] = true, } m["trans-bottom"] = empty_list m["trans-see"] = { [1] = required, [2] = list, id = list_allow_holes_separate_no_index, } m["translation subpage"] = empty_list m["no equivalent translation"] = { [1] = required_lang_default_und, noend = boolean, } m["no attested translation"] = { [1] = required_lang_default_und, noend = boolean, sort = true, } m["not used"] = { [1] = required_lang_default_und, [2] = true, } return m 51jr4evgp9f3cxhobug46hkqur9gpta Mòideal:parameters/track 828 16622 86105 2026-07-16T21:06:49Z Altronic 4137 Copy from English Wiktionary 86105 Scribunto text/plain local debug_track_module = "Module:debug/track" local string_gline_module = "Module:string/gline" local match = string.match local new_title = mw.title.new local require = require local traceback = debug.traceback local function debug_track(...) debug_track = require(debug_track_module) return debug_track(...) end local function gline(...) gline = require(string_gline_module) return gline(...) end local params_title local function get_params_title() params_title, get_params_title = new_title("parameters", 828), nil return params_title end return function(page, param_name) debug_track("parameters/" .. page) if param_name ~= nil then debug_track("parameters/" .. page .. "/" .. param_name .. "=") end -- Check through the traceback to get the calling module and function. local mod_title for line in gline((traceback())) do local mod, func = match(line, "^\t*(.-):%d+: in function (.*)$") -- Must match a conventional module, not a tail call, C function, in- -- built Scribunto file etc. If `mod` matches `mod_title`, it means the -- module has been seen before, so there is nothing else to check. if mod and not (mod_title and mod == mod_title.prefixedText) then mod_title = new_title(mod) if mod_title and mod_title.namespace == 828 and not ( mod_title == (params_title or get_params_title()) or mod_title:isSubpageOf(params_title) ) then mod = mod_title.text debug_track("parameters/" .. page .. "/" .. mod) -- traceback() encloses function names in single quotes. local funcname = match(func, "^'(.*)'$") if funcname then debug_track("parameters/" .. page .. "/" .. mod .. "/" .. funcname) return end -- WHen a function is unnamed, line numbers are given after the -- module name in angle brackets, directly after a separating -- colon. local funcline = match(func, "^<.-:(%d+)>$") if funcline then debug_track("parameters/" .. page .. "/" .. mod .. ":" .. funcline) end return end end end end ep6rfkuslbwvncu36j3fa17iqe4seb6 Mòideal:debug/track 828 16623 86107 2026-07-16T21:10:31Z Altronic 4137 Copy from English Wiktionary 86107 Scribunto text/plain -- TODO 1: normalize keys with leading spaces, which don't get removed when -- "Tracking/" is prefixed. -- TODO 2: avoid weird inputs like "", which don't register as invalid since -- "Tracking/" is a valid page title. -- TODO 3: use varargs instead of a table input, then recurse with track(). local title_make_title_module = "Module:title/makeTitle" -- Transclusion-based tracking as subpages of [[Wiktionary:Tracking]]. -- Tracked pages can be found at [[Special:WhatLinksHere/Wiktionary:Tracking/KEY]]. local error = error local find = string.find local log = mw.log local sub = string.sub local type = type local invalid_tracking_key_key = "debug/track/invalid key" local function make_title(...) make_title = require(title_make_title_module) return make_title(...) end local memo local function track(key) if not memo then memo = {} -- Return if memoized. elseif memo[key] then return end -- Throw an error if `key` isn't a string. local key_type = type(key) if key_type ~= "string" then error("Tracking keys supplied to [[Module:debug/track]] must be strings; received " .. key_type .. ".", 3) end -- make_title returns nil for invalid titles, but "#" is treated as a -- fragment separator (e.g. "foo#bar" generates the title "foo"), so it -- needs to be manually excluded. local title = not find(key, "#", nil, true) and make_title(4, "Tracking/" .. key) if not title then -- Track uses of invalid keys. Instead of recursing, simply memoize the -- invalid key and replace it with the 'invalid tracking key' key. -- [[Special:WhatLinksHere/Wiktionary:Tracking/debug/track/invalid key]] log("Invalid tracking key: " .. key) memo[key] = true key = invalid_tracking_key_key if memo[key] then return end title = make_title(4, "Tracking/" .. key) end -- Normalize the key by treating it as a subpage of "Tracking", which gives -- the normalized title without the initial "Tracking/". If the normalized -- form has been memoized, don't transclude the page again. Otherwise, -- transclude the page using the `content` key on the title, which is the -- cheapest way to trigger transclusion, as it avoids any parser expansion -- of the target page. local normalized = sub(title.text, 10) -- excludes "Tracking/" if not memo[normalized] then title = title.content memo[normalized] = true end -- Memoize the original key. memo[key] = true end return function(input) if input == nil then error("No tracking key supplied to [[Module:debug/track]].", 2) elseif type(input) ~= "table" then track(input) return true end local key = input[1] if key == nil then error("No tracking keys in table supplied to [[Module:debug/track]].", 2) end local i = 1 repeat track(key) i = i + 1 key = input[i] until key == nil return true end p6sdr83482xvakdb92gtqfoyor6nnlb Mòideal:debug/escape 828 16624 86108 2026-07-16T21:12:08Z Altronic 4137 Copy from English Wiktionary 86108 Scribunto text/plain local string_isutf8_module = "Module:string/isutf8" local byte = string.byte local dump = mw.dumpObject local error = error local format = string.format local gsub = string.gsub local sub = string.sub local type = type local function isutf8(...) isutf8 = require(string_isutf8_module) return isutf8(...) end local quote_options local function get_quote_options() quote_options, get_quote_options = { noquotes = "", single = "'", double = '"', quotes = "'\"" }, nil return quote_options end local escapes local function get_escapes() escapes, get_escapes = { ["\a"] = [[\a]], ["\b"] = [[\b]], ["\t"] = [[\t]], ["\n"] = [[\n]], ["\v"] = [[\v]], ["\f"] = [[\f]], ["\r"] = [[\r]], ['"'] = [[\"]], ["'"] = [[\']], ["\\"] = [[\\]], }, nil return escapes end -- Escapes one byte. local function escape_byte(ch) return (escapes or get_escapes())[ch] or format([[\%03d]], byte(ch)) end -- Escapes a string of bytes. local function escape_bytes(b) return (gsub(b, ".", escape_byte)) end -- Takes a valid UTF-8 character with its leading byte, and potentially escapes -- it. local function maybe_escape_char(ch, b) -- Escape the control characters (U+0080 to U+009F) and the no-break space -- (U+00A0). if b == 0xC2 and byte(ch, 2) <= 0xA0 then return escape_bytes(ch) end return ch end -- Handles a character-like raw chunk of escapable bytes. local function escape_chunk(chunk) local chunk_len = #chunk if chunk_len == 1 then return escape_byte(chunk) end local b = byte(chunk) -- If the initial byte is a 1-byte character (\x00 to \x7F) or not valid as -- a leading byte (\x80 to \xC1 or \xF5 to \xFF), escape `chunk`. if b < 0xC2 or b > 0xF4 then return escape_bytes(chunk) end -- Get the expected chunk length, which is the length of a UTF-8 character -- with leading byte `b`. local exp_len = b < 0xE0 and 2 or b < 0xF0 and 3 or 4 -- If `chunk` is the expected length, return it if it's a valid UTF-8 -- character, or escape if not. if chunk_len == exp_len then return isutf8(chunk) and maybe_escape_char(chunk, b) or escape_bytes(chunk) -- If it's too short, escape it. elseif chunk_len < exp_len then return escape_bytes(chunk) end -- If it's too long, it could be a valid UTF-8 character followed by further -- bytes. If it is, keep the valid character intact, but escape everything -- after. local init_ch = sub(chunk, 1, exp_len) if isutf8(init_ch) then return maybe_escape_char(init_ch, b) .. escape_bytes(sub(chunk, exp_len + 1)) end -- Otherwise, escape all of `chunk`. return escape_bytes(chunk) end --[==[ Escapes control characters, backslash, the no-break space, bytes that aren't used in UTF-8 and invalid UTF-8 character sequences. The optional {quotes} flag controls how quotation marks are handled, which takes a string value: * {"quotes"}: escapes {'} and {"} (default) * {"single"}: escapes {'} only * {"double"}: escapes {"} only * {"noquotes"}: no quotation mark escapes]==] return function(str, quotes) local q = (quote_options or get_quote_options())[quotes == nil and "quotes" or quotes] if not q then local quotes_type = type(quotes) error('`quotes` must be "quotes", "single", "double" or nil; received ' .. (quotes_type == "string" and dump(quotes) or "a " .. quotes_type)) end -- TODO: handle Unicode normalization. return (gsub(str, format("[%%c%s\\\128-\255][\128-\191]*", q), escape_chunk)) end rn0vj9vyr09lho2bj1ncd4ipwkhyrei Mòideal:debug/templates 828 16625 86109 2026-07-16T21:14:38Z Altronic 4137 Copy from English Wiktionary 86109 Scribunto text/plain local export = {} -- Trigger a script error from a template function export.track(frame) local params = { [1] = {required = true, list = true} } local args = require("Module:parameters").process(frame.args, params) require("Module:debug/track")(args[1]) end -- Trigger a script error from a template function export.error(frame) error(frame.args[1] or "(no message specified)") end return export dk3vfk3dzvhmdwvmjh1rw7zbgva2hzb Mòideal:Scribunto 828 16626 86110 2026-07-16T21:18:34Z Altronic 4137 Copy from English Wiktionary 86110 Scribunto text/plain local export = {} local math_module = "Module:math" local dump = mw.dumpObject local format = string.format local gsub = string.gsub local match = string.match local php_trim -- defined below local sub = string.sub local tonumber = tonumber local tostring = tostring local type = type do local php_htmlspecialchars_data local function get_php_htmlspecialchars_data() php_htmlspecialchars_data, get_php_htmlspecialchars_data = { ["\""] = "&quot;", ["&"] = "&amp;", ["'"] = "&#039;", ["<"] = "&lt;", [">"] = "&gt;", }, nil return php_htmlspecialchars_data end --[==[Lua equivalent of PHP's {{code|php|htmlspecialchars($string)}}, which converts the characters `&"'<>` to HTML entities. If the `quotes` flag is set to {"compat"}, then `'` will not be converted, and if it is set to {"noquotes"}, then neither `"` nor `'` will be converted.]==] function export.php_htmlspecialchars(str, quotes) if quotes == nil or quotes == "quotes" then quotes = "'\"" elseif quotes == "compat" then quotes = "\"" elseif quotes == "noquotes" then quotes = "" else local quotes_type = type(quotes) error('`quotes` must be "quotes", "compat", "noquotes" or nil; received ' .. (quotes_type == "string" and dump(quotes) or "a " .. quotes_type)) end return (gsub(str, "[&<>" .. quotes .. "]", php_htmlspecialchars_data or get_php_htmlspecialchars_data())) end end do local function tonumber_extended(...) tonumber_extended = require(math_module).tonumber_extended return tonumber_extended(...) end -- Normalizes a string for use in comparisons which emulate PHP's equals -- operator, which coerces certain strings to numbers: those within the -- range -2^63 to 2^63 - 1 which don't have decimal points or exponents are -- coerced to integers, while any others are coerced to doubles if possible; -- otherwise, they remain as strings. PHP and Lua have the same precision -- for doubles, but Lua's integer precision range is -2^53 + 1 to 2^53 - 1. -- Any integers within Lua's precision, as well as all doubles, are simply -- coerced to numbers, but PHP integers outside of Lua's precision are -- emulated as normalized strings, with leading 0s and any + sign removed. -- The `not_long` flag is used for the second comparator if the first did -- not get normalized to a long integer, as PHP will only coerce strings to -- integers if it's possible for both comparators. local function php_normalize_string(str, not_long) local num = tonumber_extended(str, nil, true) -- Must be a number that isn't ±infinity, NaN or hexadecimal. if not num or match(str, "^%s*[+-]?0[xX]()") then return str -- If `not_long` is set or `num` is within Lua's precision, return as a -- number. elseif not_long or num < 9007199254740992 and num > -9007199254740992 then return num, "number" end -- Check if it could be a long integer, and return as a double if not. local sign, str_no_0 = match(str, "^%s*([+-]?)0*(%d+)$") if not str_no_0 then return num, "number" end -- Otherwise, check if it's a long integer. 2^63 is 9223372036854775808, -- so slice off the last 15 digits and deal with the two parts -- separately. If the integer value would be too high/low, return as a -- string. local high = tonumber(sub(str_no_0, 1, -16)) if high > 9223 then return str elseif high == 9223 then local low = tonumber(sub(str_no_0, -15)) -- Range is -2^63 to 2^63 - 1 (not symmetrical). if low > 372036854775808 or low == 372036854775808 and sign ~= "-" then return str end end return (sign == "+" and "" or sign) .. str_no_0, "long integer", num end --[==[Lua equivalent of PHP's {{code|php|===}} operator for strings.]==] function export.php_string_equals(str1, str2) if str1 == str2 then return true end local str1, str1_type, str1_num = php_normalize_string(str1) if str1 == str2 then return true elseif str1_type == "long integer" then local str2, str2_type = php_normalize_string(str2) return str2 == (str2_type == "number" and str1_num or str1) elseif str1_type == "number" then return str1 == php_normalize_string(str2, true) end return false end end --[==[Lua equivalent of PHP's {{code|php|trim($string)}}, which trims {"\0"}, {"\t"}, {"\n"}, {"\v"}, {"\r"} and {" "}. This is useful when dealing with template parameters, since the native parser trims them like this.]==] function export.php_trim(str) return match(str, "[^ \t-\v\r%z].*%f[ \t-\v\r%z]") or "" end php_trim = export.php_trim --[==[Lua equivalent of PHP's {{code|php|ltrim($string)}}, which trims {"\0"}, {"\t"}, {"\n"}, {"\v"}, {"\r"} and {" "} from the beginning of the input string.]==] function export.php_ltrim(str) return (gsub(str, "^[ \t-\v\r%z]+", "")) end --[==[Lua equivalent of PHP's {{code|php|rtrim($string)}}, which trims {"\0"}, {"\t"}, {"\n"}, {"\v"}, {"\r"} and {" "} from the end of the input string.]==] function export.php_rtrim(str) return match(str, "^.+%f[ \t-\v\r%z]") or "" end --[==[Takes a template or module parameter name as either a string or number, and returns the Scribunto-normalized form (i.e. the key that that parameter would have in a {frame.args} table). For example, {"1"} (a string) is normalized to {1} (a number), {" foo "} is normalized to {"foo"}, and {1.5} (a number) is normalized to {"1.5"} (a string). Inputs which cannot be normalized (e.g. booleans) return {nil}. Strings are trimmed with {export.php_trim}, unless the `no_trim` flag is set. If it is, then string parameters are not trimmed, but strings may still be converted to numbers if they do not contain whitespace; this is necessary when normalizing keys into the form received by PHP during callbacks, before any trimming occurs (e.g. in the table of arguments when calling {frame:expandTemplates()}). After trimming (if applicable), keys are then converted to numbers if '''all''' of the following are true: # They are integers; i.e. no decimals or leading zeroes (e.g. {"2"}, but not {"2.0"} or {"02"}). # They are ≤ 2{{sup|53}} and ≥ -2{{sup|53}}. # There is no leading sign unless < 0 (e.g. {"2"} or {"-2"}, but not {"+2"} or {"-0"}). # They contain no leading or trailing whitespace (which may be present when the `no_trim` flag is set). Numbers are converted to strings if '''either''': # They are not integers (e.g. {1.5}). # They are > 2{{sup|53}} or < -2{{sup|53}}. When converted to strings, integers ≤ 2{{sup|63}} and ≥ -2{{sup|63}} are formatted as integers (i.e. all digits are given), which is the range of PHP's integer precision, though the actual output may be imprecise since Lua's integer precision is > 2{{sup|53}} to < -2{{sup|53}}. All other numbers use the standard formatting output by {tostring()}.]==] function export.scribunto_parameter_key(key, no_trim) local key_type = type(key) if key_type == "string" then if not no_trim then key = php_trim(key) end if match(key, "^()-?[1-9]%d*$") then local num = tonumber(key) -- Lua integers are only precise to 2^53 - 1, so specifically check -- for 2^53 and -2^53 as strings, since a numerical comparison won't -- work as it can't distinguish 2^53 from 2^53 + 1. return ( num <= 9007199254740991 and num >= -9007199254740991 or key == "9007199254740992" or key == "-9007199254740992" ) and num or key end return key == "0" and 0 or key elseif key_type == "number" then -- No special handling needed for inf or NaN. return key % 1 == 0 and ( key <= 9007199254740992 and key >= -9007199254740992 and key or key <= 9223372036854775808 and key >= -9223372036854775808 and format("%d", key) ) or tostring(key) end return nil end --[==[Takes a template or module parameter value as either a string, number or boolean, and returns the Scribunto-normalized form (i.e. the value that that parameter would have in a {frame.args} table), which is always a string. For example, {"foo"} remains the same, {2} (a number) is normalized to {"2"} (a string), {true} is normalized to {"1"}, and {false} is normalized to {""}. Inputs which cannot be normalized (e.g. tables) return {nil}. By default, returned values are not trimmed, which matches the treatment of unnamed parameters (e.g. `bar` in {{tl|<nowiki/>foo|bar}}). If the `named` flag is set, then returned values will be trimmed, which matches the treatment of named parameters (e.g. `baz` in {{tl|<nowiki/>foo|bar=baz}}).]==] function export.scribunto_parameter_value(value, named) local value_type = type(value) if value_type == "string" then return named and php_trim(value) or value elseif value_type == "number" then return tostring(value) elseif value_type == "boolean" then return value and "1" or "" end return nil end return export 90dvo9iqmpdordtuzghbnnl9rw4476r Mòideal:Scribunto/metamethods 828 16627 86111 2026-07-16T21:19:32Z Altronic 4137 Copy from English Wiktionary 86111 Scribunto text/plain return { __add = true, __call = true, __concat = true, __div = true, __eq = true, __gc = true, __index = true, __ipairs = true, __le = true, __len = true, __lt = true, __metatable = true, __mod = true, __mode = true, __mul = true, __newindex = true, __pairs = true, __pow = true, __sub = true, __tostring = true, __unm = true, } 9ldqvu9rhlfkuksyro1b52m6ychbevm Mòideal:Scribunto/types 828 16628 86112 2026-07-16T21:24:23Z Altronic 4137 Copy from English Wiktionary 86112 Scribunto text/plain return { ["boolean"] = true, ["function"] = true, ["nil"] = true, ["number"] = true, ["proto"] = true, ["string"] = true, ["table"] = true, ["thread"] = true, ["upval"] = true, ["userdata"] = true, } h3jolro506uslruy8jg71dg8sfq1mkf Mòideal:math 828 16629 86113 2026-07-16T21:29:58Z Altronic 4137 Copy from English Wiktionary 86113 Scribunto text/plain local export = {} local byte = string.byte local ceil = math.ceil local floor = math.floor local format = string.format local is_integer -- defined below local match = string.match local select = select local tonumber = tonumber local tonumber_ext -- defined below local tostring = tostring local type = type local INF = math.huge local function sign(x, signed_0) if x > 0 then return 1 elseif x < 0 then return -1 elseif x == 0 then -- 1/(+0) is infinity and 1/(-0) is -infinity. return signed_0 and (1 / x > 0 and 1 or -1) or 0 end -- NaN: convert to string with a forced sign prefix, and grab the first byte. local sign = byte(format("%+f", x)) return sign == 0x2B and 1 or -- + sign == 0x2D and -1 or -- - -- If there's no sign, throw an error. This shouldn't be possible, but -- avoids silent errors if it does happen. error("Internal error: cannot determine sign of " .. x) end --[==[ An extended version of {tonumber()}, which attempts to convert `x` to a number. Like {tonumber()}, it will convert from base 10 by default, and the optional parameter `base` can be used to specify a different base between 2 and 36, with the letters {A-Z} (case-insensitive) representing additional digits beyond {0-9}. When strings contain hexadecimal notation (e.g. {"0x100"}), base 16 is used as the default instead, but this is overridden if `base` is set to anything other than 16. This function differs from {tonumber()} in the following ways: * If `finite_real` is set, then the function will only return finite real numbers; inputs which would normally produce ±infinity or NaN will instead produce {nil}. * If `no_prefix` is set, then strings which start with {"0x"} will not be interpreted as containing hexadecimal notation, resulting in {nil}. * If `base` is explicitly set to {10}, then strings in hexadecimal notation will always return {nil}. This fixes a bug in {tonumber()}, which treats {base=10} the same as {base} being unset, causing base 16 to be used if `x` contains hexadecimal notation (e.g. {tonumber("0x10", 10)} returns {16}, whereas {tonumber_extended("0x10", 10)} returns {nil}).]==] function export.tonumber_extended(x, base, finite_real, no_prefix) -- TODO: tonumber() maxes out at 2^64 if the base is anything other than 10. -- TODO: support binary (0b) and octal (0o) prefixes. local n = tonumber(x, base) if not n or finite_real and (n ~= n or n == INF or n == -INF) then return nil -- If `base` is explicitly set to 10 (not simply nil), or `no_prefix` is set -- and `base` is nil or 16, filter out inputs that started with hexadecimal -- prefixes. Note that if `base` is anything else, the initial "0x" will -- have been interpreted as digits by tonumber() instead of a prefix (as "x" -- can be a digit from base 34 upwards), so there's no prefix to check for. elseif base == 10 or no_prefix and (base == nil or base == 16) then return not match(x, "^%s*[+-]?0[xX]()") and n or nil end return n end tonumber_ext = export.tonumber_extended --[==[ Converts `x` to an integer by removing the fractional portion (e.g. {3.5} becomes {3}, and {-2.9} becomes {-2}). This is equivalent to rounding down positive numbers and rounding up negative numbers. If conversion is not possible, returns {nil}.]==] function export.to_integer(x) x = tonumber(x) if not (x and x == x and x ~= INF and x ~= -INF) then return nil elseif x % 1 == 0 then return x -- Round-down positives. elseif x >= 0 then return floor(x) end --Round-up negatives. return ceil(x) end --[==[ Returns {1} if `x` is positive, {-1} if `x` is negative, or {0} if `x` is {0}. If `signed_0` is set, this function will only return either {1} or {-1}, and will make a distinction between [[w:signed zero|signed zeroes]] ({+0} and {-0}). This is useful when a {0} result could be disruptive (e.g. {x % 0}).]==] function export.sign(x, signed_0) return sign( tonumber(x) or error(format("bad argument #1 to 'sign' (number expected, got %s)", type(x)), 2), signed_0 ) end --[==[ Returns {true} if `x` is a finite real number, or {false} if not.]==] function export.is_finite_real_number(x) return x and x == x and not (x == INF or x == -INF) and type(x) == "number" end --[==[ Returns {true} if `x` is an integer, or {false} if not.]==] function export.is_integer(x) return x and type(x) == "number" and x % 1 == 0 or false end is_integer = export.is_integer --[==[ Returns {true} if `x` is a positive integer (or zero if the `include_0` flag is set), or {false} if not.]==] function export.is_positive_integer(x, include_0) return x and type(x) == "number" and (x > 0 or include_0 and x == 0) and x % 1 == 0 or false end --[==[ Returns {true} is `x` is [[w:NaN|NaN]] (Not a Number), or {false} if not. NaN is a value that has the type "number", but does not represent an actual numeric value; it has the unique property that if {x} is NaN, {x ~= x} evaluates to {true}.]==] function export.is_NaN(x) return x ~= x end --[==[ Returns the base-10 logarithm of `x`. This function should be used instead of {math.log10}, which is deprecated and may stop working if Scribunto is updated to a more recent Lua version.]==] function export.log10(x) -- Structured like this so that module documentation works. local log10 = math.log10 if log10 ~= nil then return log10 end local log = math.log return log(10, 10) == 1 and function(x) -- Lua 5.2 return log(x, 10) end or function(x) -- Lua 5.1 return log(x) * 0.43429448190325182765112891891660508229439700580367 -- log10(e) end end export.log10 = export.log10() -- Sets the actual returned function. local function integer_error(x, param, func_name) local type_x = type(x) error(format( "bad argument #%d to '%s' (integer expected, got %s)", param, func_name, type_x == "number" and tostring(x) or type_x ), 3) end --[==[ Converts a decimal number to hexadecimal. If `include_prefix` is set, the returned number will include the 0x prefix.]==] function export.to_hex(dec, include_prefix) dec = tonumber(dec) or dec if not is_integer(dec) then integer_error(dec, 1, "to_hex") end local neg = dec < 0 if neg then dec = -dec end -- Inputs >= 2^64 cause string.format to return "0". if dec >= 0x1p64 then error("integer overflow in 'to_hex': cannot convert inputs with a magnitude greater than or equal to 2^64 (18446744073709551616)", 2) end -- string.format treats hex numbers as unsigned, so any sign must be added manually. return format("%s%s%X", neg and "-" or "", include_prefix and "0x" or "", dec) end --[==[ Returns the greatest common divisor of an arbitrary number of input numbers.]==] function export.gcd(x, ...) x = tonumber(x) or x if not is_integer(x) then integer_error(x, 1, "gcd") end local q, args_len, integers = ..., select("#", ...) -- Compute p_1 = gcd(n_1, n_2), p_2 = gcd(p_1, n_3), ... i.e. compute GCD by Euclid's algorithm for the current result and the next number. for i = 2, args_len + 1 do q = tonumber(q) or q if not is_integer(q) then integer_error(q, i, "gcd") elseif x ~= 1 then -- If x is 1, validate remaining inputs. -- GCD of two integers x, q with Euclid's algorithm. while q ~= 0 do x, q = q, x % q end end if i <= args_len then -- Only create a table if absolutely necessary, as it's inefficient. if i == 2 then integers = {...} end q = integers[i] end end return x < 0 and -x or x end --[==[ Returns the least common multiple of an arbitrary number of input numbers.]==] function export.lcm(x, ...) x = tonumber(x) or x if not is_integer(x) then integer_error(x, 1, "lcm") end local q, args_len, integers = ..., select("#", ...) -- Compute the product of all inputs as p and GCD as x. for i = 2, args_len + 1 do q = tonumber(q) or q if not is_integer(q) then integer_error(q, i, "lcm") elseif x ~= 0 then -- If x is 0, validate remaining inputs. -- Compute the product. local p = x * q -- GCD of two integers x, q with Euclid's algorithm. while q ~= 0 do x, q = q, x % q end -- Divide product by the GCD to get new LCM. x = p / x end if i <= args_len then -- Only create a table if absolutely necessary, as it's inefficient. if i == 2 then integers = {...} end q = integers[i] end end return x < 0 and -x or x end return export 9iueib95u6jd0lc3zzrya3gsltprarl Mòideal:math/compare 828 16630 86114 2026-07-16T21:30:40Z Altronic 4137 Chaidh duilleag le "local math_module = "Module:math" local function is_NaN(...) is_NaN = require(math_module).is_NaN return is_NaN(...) end local function sign(...) sign = require(math_module).sign return sign(...) end --[==[ A comparison function for numbers, which returns {true} if {a} sorts before {b}, or otherwise {false}; it can be used as the sort function with {table.sort}. This function is roughly equivalent to the {<} operator, but contains the following sp..." a chruthachadh 86114 Scribunto text/plain local math_module = "Module:math" local function is_NaN(...) is_NaN = require(math_module).is_NaN return is_NaN(...) end local function sign(...) sign = require(math_module).sign return sign(...) end --[==[ A comparison function for numbers, which returns {true} if {a} sorts before {b}, or otherwise {false}; it can be used as the sort function with {table.sort}. This function is roughly equivalent to the {<} operator, but contains the following special considerations in accordance with the {{w|IEEE 754}} standard: * {{w|NaN}} is sorted as though it has a larger absolute value than infinity ({-NaN < -Inf}; {+Inf < +NaN}). * {{w|Signed zero}} is acknowledged, with {-0 < +0}.]==] return function(a, b) -- <, > and == canot return true if either `a` or `b` are NaN. if a < b then return true -- Use > then == instead of >=, so that the ±0 check is only done when `a` -- and `b` are equal. elseif a > b then return false elseif a == b then -- 1/(+0) is Inf; 1/(-0) is -Inf. return a == 0 and b == 0 and 1 / a < 1 / b or false -- One or both must be NaN, and NaN is the only number that returns false -- to a self-equality check, so if `a` == `a` then `b` is NaN (and vice- -- versa). -NaN sorts before everything and +NaN after everything, so the -- sign determines the result. elseif not is_NaN(a) then -- `b` is NaN return sign(b) == 1 elseif not is_NaN(b) then -- `a` is NaN return sign(a) == -1 end -- If both are NaN, only return true if `a` is -NaN and `b` is +NaN. return sign(a) < sign(b) end 632obodjr2zjfbz9zpr3erx1io9vwm4 Mòideal:math/testcases 828 16631 86115 2026-07-16T21:32:36Z Altronic 4137 Copy from English Wiktionary 86115 Scribunto text/plain local tests = require("Module:UnitTests") local m_math = require("Module:math") local concat = table.concat local dump = mw.dumpObject local gcd = m_math.gcd local highlight = require("Module:debug").highlight local lcm = m_math.lcm local sign = m_math.sign local tonumber = tonumber local unpack = unpack or table.unpack -- Lua 5.2 compatibility local INF = math.huge local NEG_INF = -INF local NAN = tonumber("nan") local NEG_NAN = tonumber("-nan") local function do_test(func, args, expected, name) if name == nil then name = {} for i, v in ipairs(args) do name[i] = dump(v) end name = highlight("(" .. concat(name, ", ") .. ")") end tests:equals(name, func(unpack(args)), expected) end function tests:check_sign(args, expected, name) return do_test(sign, args, expected, name) end function tests:test_sign() self:iterate({ {{1}, 1}, {{5}, 1}, {{1.3}, 1}, {{0.8}, 1}, {{100}, 1}, {{1e308}, 1}, {{INF}, 1}, {{NAN}, 1}, {{"1"}, 1}, {{"2.5"}, 1}, {{"inf"}, 1}, {{"nan"}, 1}, {{0}, 0}, {{"0"}, 0}, {{0, true}, 1}, {{"0", true}, 1}, {{-1}, -1}, {{-5}, -1}, {{-1.3}, -1}, {{-0.8}, -1}, {{-100}, -1}, {{-1e308}, -1}, {{NEG_INF}, -1}, {{NEG_NAN}, -1}, {{"-1"}, -1}, {{"-2.5"}, -1}, {{"-inf"}, -1}, {{"-nan"}, -1}, {{tonumber("-0")}, 0}, {{"-0"}, 0}, {{tonumber("-0"), true}, -1}, {{"-0", true}, -1}, }, "check_sign") end function tests:check_gcd(args, expected, name) return do_test(gcd, args, expected, name) end function tests:test_gcd() self:iterate({ {{1}, 1}, {{-1}, 1}, {{0}, 0}, {{0, 0}, 0}, {{1, 0}, 1}, {{0, 1}, 1}, {{1, 1}, 1}, {{6, 4}, 2}, {{6, -4}, 2}, {{-6, -4}, 2}, {{2, 8}, 2}, {{15, 20}, 5}, {{20, 15}, 5}, {{35, -21}, 7}, {{48, 18}, 6}, {{8, 12, 16}, 4}, {{25, -35, 95}, 5}, {{95, -35, 25}, 5}, {{1500, 750, 150000, 625}, 125}, {{186028, 193052, 144624}, 4}, {{2^100, 2^53}, 2^53, "2^100, 2^53"}, }, "check_gcd") end function tests:check_lcm(args, expected, name) return do_test(lcm, args, expected, name) end function tests:test_lcm() self:iterate({ {{1}, 1}, {{-1}, 1}, {{0}, 0}, {{0, 0}, 0}, {{1, 0}, 0}, {{0, 1}, 0}, {{1, 1}, 1}, {{6, 4}, 12}, {{6, -4}, 12}, {{-6, -4}, 12}, {{2, 8}, 8}, {{15, 20}, 60}, {{20, 15}, 60}, {{35, -21}, 105}, {{48, 18}, 144}, {{8, 12, 16}, 48}, {{25, -35, 95}, 3325}, {{95, -35, 25}, 3325}, {{1500, 750, 150000, 625}, 150000}, {{186028, 193052, 144624}, 324618307124784}, {{2^100, 2^53}, 2^100, "2^100, 2^53"}, }, "check_lcm") end return tests dgzgyb0h9ptzp6pt63h7xut5p2qy4ah Mòideal:collation 828 16632 86116 2026-07-16T21:51:30Z Altronic 4137 Copy from English Wiktionary 86116 Scribunto text/plain local export = {} local compare_module = "Module:compare" local functions_module = "Module:fun" local memoize_module = "Module:memoize" local string_utilities_module = "Module:string utilities" local utilities_module = "Module:utilities" local concat = table.concat local find = string.find local format = string.format local make_sort_function -- defined below local match = string.match local remove = table.remove local require = require local sort = table.sort local sub = string.sub local type = type local function get_plaintext(...) get_plaintext = require(utilities_module).get_plaintext return get_plaintext(...) end local function is_callable(...) is_callable = require(functions_module).is_callable return is_callable(...) end local function memoize(...) memoize = require(memoize_module) return memoize(...) end local function trim(...) trim = require(string_utilities_module).trim return trim(...) end -- Custom functions for generating a sortkey that will achieve the desired sort -- order. local custom_funcs local function get_custom_funcs() custom_funcs, get_custom_funcs = { ahk = "Mymr-sortkey", aio = "Mymr-sortkey", blk = "Mymr-sortkey", egy = "egy-utilities", kac = "Mymr-sortkey", kht = "Mymr-sortkey", ksw = "Mymr-sortkey", kyu = "Mymr-sortkey", ["mkh-mmn"] = "Mymr-sortkey", mnw = "Mymr-sortkey", my = "Mymr-sortkey", phk = "Mymr-sortkey", pwo = "Mymr-sortkey", omx = "Mymr-sortkey", shn = "Mymr-sortkey", tjl = "Mymr-sortkey", }, nil return custom_funcs end local function is_lang_object(lang) return lang and type(lang) == "table" and type(lang.getCanonicalName) == "function" end local function check_function(funcName, argIdx, func) return is_callable(func) or error(format("bad argument #%d to %s: expected function or callable table; got %s", argIdx, funcName, type(func)), 2) end function export.make_lang_sortkey_function(lang, make_sortbase) local makeDisplayText, makeSortKey = lang.makeDisplayText local custom = (custom_funcs or get_custom_funcs())[lang:getCode()] if custom then local _makeSortKey = require("Module:" .. custom).makeSortKey function makeSortKey(_, text) return _makeSortKey(text, lang, lang:findBestScript(text)) end else makeSortKey = lang.makeSortKey end return make_sortbase and check_function("make_sort_function", 3, make_sortbase) and function(element) return (makeSortKey( lang, (makeDisplayText( lang, get_plaintext(make_sortbase(element)) )) )) end or function(element) return (makeSortKey( lang, (makeDisplayText( lang, get_plaintext(element) )) )) end end do local compare local function get_compare() compare, get_compare = require(compare_module), nil return compare end function export.make_sort_function(lang, make_sortbase) local compare_func, make_sortkey = compare or get_compare() if is_lang_object(lang) then make_sortkey = memoize(export.make_lang_sortkey_function(lang, make_sortbase), true) elseif make_sortbase and check_function("make_sort_function", 2, make_sortbase) then make_sortkey = memoize(make_sortbase, true) else return compare_func end return function(elem1, elem2) return compare_func(make_sortkey(elem1), make_sortkey(elem2)) end end make_sort_function = export.make_sort_function end function export.sort(elems, lang, make_sortbase) return sort(elems, make_sort_function(lang, make_sortbase)) end function export.sort_template(frame) if not mw.isSubsting() then error("This template must be substed.") end local args = (frame.args.parent and frame:getParent() or frame).args local m_table = require("Module:table") local elems = m_table.shallowCopy(args) local code, lang_param = args.lang if code then -- List starts at 1. lang_param = "lang" else -- List starts at 2: retrieve `code` from key 1 with remove(). which lang_param = 1 code = remove(elems, 1) -- remove() also shifts the remaining elements down by 1 as a side- -- effect, so the list now starts at 1. end if code then trim(code) end local lang = require("Module:languages").getByCode(code, lang_param, true) local i = 1 while true do local elem = elems[i] while elem do elem = trim(elem, "%s") if elem ~= "" then break end remove(elems, i) elem = elems[i] end if not elem then break elseif not ( -- Strip redundant wikilinks. not match(elem, "^()%[%[") or find(elem, "[[", 3, true) or find(elem, "]]", 3, true) ~= #elem - 1 or find(elem, "|", 3, true) ) then elem = sub(elem, 3, -3) elem = trim(elem, "%s") end elems[i] = elem .. "\n" i = i + 1 end elems = m_table.removeDuplicates(elems) export.sort(elems, lang) return concat(elems, args.sep or "|") end return export 1329hn2ohk3bwec6f4512tyaf8hrk1j Mòideal:compare 828 16633 86117 2026-07-16T21:55:37Z Altronic 4137 Copy from English Wiktionary 86117 Scribunto text/plain local math_compare_module = "Module:math/compare" local string_compare_module = "Module:string/compare" local table_compare_module = "Module:table/compare" local require = require local type = type local types local function get_types() types, get_types = { ["nil"] = "\1", ["boolean"] = "\2", ["number"] = "\3", ["string"] = "\4", }, nil return types end local compare_funcs local function get_compare_funcs() compare_funcs = {} function compare_funcs.boolean(a, b) return a == true and b == false end function compare_funcs.number(...) local math_compare = require(math_compare_module) compare_funcs.number = math_compare return math_compare(...) end function compare_funcs.string(...) local string_compare = require(string_compare_module) compare_funcs.string = string_compare return string_compare(...) end function compare_funcs.table(...) local table_compare = require(table_compare_module) compare_funcs.table = table_compare return table_compare(...) end get_compare_funcs = nil return compare_funcs end --[==[ A general comparison function, which returns {true} if {a} sorts before {b}, or otherwise {false}; it can be used as the sort function with {table.sort}. This function is roughly equivalent to the {<} operator, but is capable of comparing a mix of types, using the following rules: * When items of different types are compared, the type-order is {"nil"}, {"boolean"}, {"number"}, {"string"}, then alphabetical order by type name for all other types. * Numbers are compared with [[Module:math/compare]]. * Strings are compared with [[Module:string/compare]]. * Tables are compared with [[Module:table/compare]]. * Boolean {true} sorts before {false}. * All other types always return {false}.]==] return function(a, b) local type_a, type_b = type(a), type(b) if type_a == type_b then local func = (compare_funcs or get_compare_funcs())[type_a] return func and func(a, b) or false end return ((types or get_types())[type_a] or type_a) < (types[type_b] or type_b) end fs82yur52kblfpa4j6uwsb5dbcxr7em Mòideal:string/compare 828 16634 86118 2026-07-16T22:09:54Z Altronic 4137 Copy from English Wiktionary 86118 Scribunto text/plain local byte = string.byte local match = string.match local sub = string.sub --[==[ A comparison function for strings, which returns {true} if {a} sorts before {b}, or otherwise {false}; it can be used as the sort function with {table.sort}. This function always sorts using byte-order, which makes it roughly equivalent to the {<} operator, but with fixes for two serious bugs raised in [[phab:T193096#4161287]] and [[phab:T49137#9167559]]: * {<} is supposed to compare UTF-8 codepoints in the two strings, but when a codepoint that is U+10000 or above is encountered in the left-hand string, {<} always returns {false}, irrespective of the content of the other string. * {<} treats unassigned codepoints and non-UTF-8 byte sequences as being higher than {"\0"} but lower than {"\1"}, instead of sorting according to byte order.]==] return function(a, b) -- Equality check. if a == b then return false end -- Byte comparison is slow, so only do it when it's really needed: -- iterate over both strings, grabbing a set of ASCII bytes followed by -- a set of non-ASCII bytes from each (either of which could be empty), -- and compare them with ==. If the ASCII substrings are unequal, just -- use <, since the bug won't affect it. Otherwise, compare bytes in the -- non-ASCII substrings. local loc, ascii_a, nonascii_a, ascii_b, nonascii_b = 1 repeat ascii_a, nonascii_a = match(a, "^([^\128-\255]*)([\128-\255]*)", loc) ascii_b, nonascii_b, loc = match(b, "^([^\128-\255]*)([\128-\255]*)()", loc) -- update `loc` on the second call -- When comparing ASCII sets, use <. The lower substring will be -- from the lower string *except* when it comprises the start of the -- other substring and is followed by a non-ASCII character. For -- instance, if `ascii_a` is "pqrs": -- If `ascii_b` is "abc", `b` is lower, since "abc" < "pqrs". -- If `ascii_b` is "pqr" and followed by non-ASCII "ž", `a` is -- lower, since "pqrs" < "pqrž". -- If `ascii_b` is "pqr" and at the end of `b`, `b` is lower, since -- "pqr" < "pqrs". if ascii_a ~= ascii_b then if ascii_a < ascii_b then return nonascii_a == "" or ascii_a ~= sub(ascii_b, 1, #ascii_a) end return not (nonascii_b == "" or ascii_b ~= sub(ascii_a, 1, #ascii_b)) end -- If the non-ASCII parts are not equal, terminate the loop. until nonascii_a ~= nonascii_b -- If either one is the empty string, then the end of that string has -- been reached, making it the lower string. if nonascii_a == "" then return true elseif nonascii_b == "" then return false end loc = 1 while true do -- 4 bytes at a time is a balance between minimizing the number of -- byte() calls without grabbing unnecessary extra bytes after the -- difference. local b_a1, b_a2, b_a3, b_a4 = byte(nonascii_a, loc, loc + 3) if b_a1 == nil then return true end local b_b1, b_b2, b_b3, b_b4 = byte(nonascii_b, loc, loc + 3) if b_a1 ~= b_b1 then return b_b1 and b_a1 < b_b1 elseif b_a2 ~= b_b2 then return b_a2 == nil or b_b2 and b_a2 < b_b2 elseif b_a3 ~= b_b3 then return b_a3 == nil or b_b3 and b_a3 < b_b3 elseif b_a4 ~= b_b4 then return b_a4 == nil or b_b4 and b_a4 < b_b4 end loc = loc + 4 end end fshgty7mojs150t5jnkyy2f1rbcgty2 Mòideal:usex/templates 828 16635 86119 2026-07-16T23:00:51Z Altronic 4137 Copy from English Wiktionary 86119 Scribunto text/plain local export = {} local table_module = "Module:table" local usex_module = "Module:usex" local yesno_module = "Module:yesno" local insert = table.insert local rfind = mw.ustring.find local rsplit = mw.text.split local function track(page) require("Module:debug/track")("usex/templates/" .. page) return true end function export.usex_t(frame) local boolean = {type = "boolean"} local list = {list = true} local language = {type = "language"} local script = {type = "script"} -- Invocation arguments (passed in the template #invoke call). local iargs = require("Module:parameters").process(frame.args, { ["quote"] = true, ["inline"] = true, ["nocat"] = boolean, ["class"] = true, }) local parent_args = frame:getParent().args if parent_args.qualifier then track("qualifier") end -- Template (parent) arguments. local args = require("Module:parameters").process(parent_args, { -- Usex/quotation text parameters [1] = {required = true, type = "language"}, [2] = true, ["termlang"] = language, ["tr"] = true, ["transliteration"] = {alias_of = "tr", deprecated = true}, ["ts"] = true, ["transcription"] = {alias_of = "ts", deprecated = true}, ["sc"] = script, ["norm"] = true, ["normalization"] = {alias_of = "norm", deprecated = true}, ["normsc"] = script, ["subst"] = true, ["q"] = list, ["qualifier"] = {alias_of = "q", list = false, deprecated = true}, ["qq"] = list, ["ref"] = true, -- Usex/quotation audio parameters, ["audio"] = true, -- Translation of usex text [3] = true, ["t"] = {alias_of = 3}, ["translation"] = {alias_of = 3, deprecated = true}, ["lit"] = true, -- Original text, if the usex/quotation is a translation ["orig"] = true, ["origlang"] = language, ["origtr"] = true, ["origts"] = true, ["origsc"] = script, ["orignorm"] = true, ["orignormsc"] = script, ["origsubst"] = true, ["origq"] = list, ["origqq"] = list, ["origref"] = true, -- Citation-related parameters; for anything more complex, usex {{quote-*}} ["source"] = true, ["footer"] = true, -- Formatting parameters ["inline"] = true, ["brackets"] = boolean, -- Categorization parameters ["nocat"] = boolean, ["sort"] = true, }) local lang = args[1] local sc = args.sc local normsc = args.normsc if normsc and not args.norm then error("Cannot specify normsc= without norm=") end if #args.qq > 0 then track("qq") end if #args.q > 0 then track("q") end local termlang = args.termlang if termlang then insert(args.qq, 1, "in " .. lang:getCanonicalName()) end local origlang, origsc, orignormsc if args.orig then origlang = args.origlang insert(args.origqq, 1, "in " .. origlang:getCanonicalName()) origsc = args.origsc orignormsc = args.orignormsc if orignormsc and not args.orignorm then error("Cannot specify orignormsc= without orignorm=") end else for _, noparam in ipairs { "origlang", "origtr", "origts", "origsc", "orignorm", "orignormsc", "origsubst", "origref" } do if args[noparam] then error(("Cannot specify %s= without orig="):format(noparam)) end end if #args.origq > 0 then error("Cannot specify origq= without orig=") end if #args.origqq > 0 then error("Cannot specify origqq= without orig=") end end local inline = args.inline or iargs.inline if inline ~= "auto" then inline = require(yesno_module)(inline) end local data = { lang = lang, termlang = termlang, sc = sc, normsc = normsc, usex = args[2], translation = args[3], transliteration = args.tr, transcription = args.ts, normalization = args.norm, inline = inline, ref = args.ref, quote = iargs.quote, lit = args.lit, subst = args.subst, -- FIXME, change to left and right qualifiers qq = #args.qq > 0 and args.qq or args.q, audio = args.audio, source = args.source, footer = args.footer, nocat = args.nocat or iargs.nocat, brackets = args.brackets, sortkey = args.sort, class = iargs.class, -- Original text, if the usex/quotation is a translation orig = args.orig, origlang = origlang, origtr = args.origtr, origts = args.origts, origsc = origsc, orignorm = args.orignorm, orignormsc = orignormsc, origsubst = args.origsubst, origq = args.origq, origqq = args.origqq, origref = args.origref, } return require(usex_module).format_usex(data) end local ignore_prefixes = {"User:", "Talk:", "Wiktionary:Beer parlour", "Wiktionary:Translation requests", "Wiktionary:Grease pit", "Wiktionary:Etymology scriptorium", "Wiktionary:Information desk", "Wiktionary:Tea room", "Wiktionary:Requests for", "Wiktionary:Votes" } function export.page_should_be_ignored(page) -- Ignore user pages, talk pages and certain Wiktionary pages for _, ip in ipairs(ignore_prefixes) do if rfind(page, "^" .. ip) then return true end end if rfind(page, " talk:") then return true end return false end function export.page_should_be_ignored_t(frame) return export.page_should_be_ignored(frame.args[1]) and "true" or "" end return export ecrgr3hi3y88hpinfqlatl83m12h2i8 Teamplaid:ux 10 16636 86120 2026-07-16T23:27:14Z Altronic 4137 Copy from English Wiktionary 86120 wikitext text/x-wiki <includeonly>{{#invoke:usex/templates|usex_t}}</includeonly><noinclude>{{documentation}}</noinclude> 3aj9jheju5tain2njzvf9cyqnvo368k Mòideal:usex 828 16637 86121 2026-07-16T23:30:44Z Altronic 4137 Copy from English Wiktionary 86121 Scribunto text/plain local export = {} local debug_track_module = "Module:debug/track" local links_module = "Module:links" local scripts_module = "Module:scripts" local script_utilities_module = "Module:script utilities" local string_utilities_module = "Module:string utilities" local usex_data_module = "Module:usex/data" local m_str_utils = require(string_utilities_module) local rsubn = m_str_utils.gsub local rsplit = m_str_utils.split local rfind = m_str_utils.find local uupper = m_str_utils.upper local ulen = m_str_utils.len local u = m_str_utils.char local translit_data = mw.loadData("Module:transliteration/data") local needs_translit = translit_data[1] local BRACKET_SUB = u(0xFFF0) local original_text = "<small>''original:''</small> " -- 100 more or less corresponds to the setting of 30 for the example text alone as formerly used in -- {{hi-x}} and {{ur-x}}, taking into account transliteration, gloss and formatting characters. -- FIXME: We should have different widths for desktop vs. mobile and generate the appropriate CSS so -- both are handled correctly. local MAX_INLINE_WIDTH = 100 -- In characters. HACK! FIXME! Do this a better way. -- List of scripts whose characters are double-width/full-width. local double_width_scripts = {"Hani", "Hrkt", "Hang"} -- microformat2 classes, see https://phabricator.wikimedia.org/T138709 local css_classes = { container_ux = 'h-usage-example', container_quotation = 'h-quotation', example = 'e-example', quotation = 'e-quotation', quotation_with_citation = 'e-quotation cited-passage', translation = 'e-translation', -- The following are added by [[Module:script utilities]], using [[Module:script utilities/data]] -- transliteration = 'e-transliteration', -- transcription = 'e-transcription', normalization = 'e-normalization', literally = 'e-literally', qualifier = 'e-qualifier', source = 'e-source', footer = 'e-footer' } -- helper functions local function track(page, code) local tracking_page = "usex/" .. page local debug_track = require(debug_track_module) debug_track(tracking_page) if code then debug_track(tracking_page .. "/" .. code) end return true end -- version of rsubn() that discards all but the first return value local function rsub(term, foo, bar) local retval = rsubn(term, foo, bar) return retval end local function wrap(tag, class, text, lang) if lang then lang = ' lang="' .. lang .. '"' else lang = "" end if text and class then return table.concat{'<', tag, ' class="', class, '"', lang, '>', text, '</', tag, '>'} else return nil end end local function span(class, text) return wrap('span', class, text) end local function div(class, text) return wrap('div', class, text) end -- Remove any HTML from the formatted text and resolve links, since the extra characters don't contribute to the -- displayed length. local function convert_to_raw_text(text) text = rsub(text, "<.->", "") if text:find("%[%[") then text = require(links_module).remove_links(text) end return text end local function get_character_width(text) local charsets = {} for _, script in ipairs(double_width_scripts) do table.insert(charsets, require(scripts_module).getByCode(script):getCharacters()) end local single_width_chars = ulen(rsub(text, "[" .. table.concat(charsets) .. "]", "")) local total_chars = ulen(text) local double_width_chars = total_chars - single_width_chars return single_width_chars + 2 * double_width_chars end --[==[ Apply the substitutions in `subst` (from the {{para|subst}} parameter or similar) to the example or quotation in `usex` after removing links, returning the resulting text. `track`, if supplied, is a function of one argument that is used to insert tracking categories: one for any call to this function, another if a single / is used in the `subst` argument. ]==] function export.apply_subst(usex, subst, track) local subbed_usex = require(links_module).remove_links(usex) local function do_track(page) if track then track(page) end return true end if subst then -- [[Special:WhatLinksHere/Wiktionary:Tracking/usex/subst]] do_track("subst") subst = rsplit(subst, ",") for _, subpair in ipairs(subst) do -- [[Special:WhatLinksHere/Wiktionary:Tracking/usex/subst-single-slash]] local subsplit = rsplit(subpair, rfind(subpair, "//") and "//" or do_track("subst-single-slash") and "/") subbed_usex = rsub(subbed_usex, subsplit[1], subsplit[2]) end end return subbed_usex end --[=[ Process parameters for usex text (either the primary text or the original text) and associated annotations. On input, the following fields are recognized in `data` (all are optional except as marked): * `lang`: Language object of text; may be an etymology language (REQUIRED). * `termlang`: The language object of the term being illustrated, which may be different from the language of the main quotation text and should always be based off of the main text, not the original text. Used for categories. May be an etymology language (REQUIRED). * `usex`: Text of usex/quotation. * `sc`: Script object of text. * `tr`: Manual transliteration. * `ts`: Transcription. * `norm`: Normalized version of text. * `normsc`: Script object of normalized version of text, or "auto". * `subst`: String of substitutions for transliteration purposes. * `quote`: If non-nil, this is a quotation (using {{tl|quote}} or {{tl|quote-*}}) instead of a usage example (using {{tl|usex}}). If it has the specific value "quote-meta", this is a quotation with citation (invoked from {{tl|quote-*}}). This controls the CSS class used to display the quotation, as well as the face used to tag the usex (which in turn results in the usex being upright text if a quotation, and italic text if a usage example). * `title`: Title object of the current page (REQUIRED). * `q`: List of left qualifiers. * `qq`: List of right qualifiers. * `ref`: String to display directly after any right qualifier, with no space. (FIXME: Should be converted into an actual ref.) * `nocat`: Overall `data.nocat` value. * `categories`: List to insert categories into (REQUIRED). * `example_type`: Either "quotation" (if `quote` specified) or "usage example" (otherwise) (REQUIRED). On output, return an object with four fields: * `usex`: Formatted usex, including qualifiers attached to both sides and `ref` attached to the right. Always specified. * `tr`: Formatted transliteration; may be nil. * `ts`: Formatted transcription; may be nil. * `norm`: Formatted normalized version of usex; may be nil. ]=] local function process_usex_text(data) local lang = data.lang local termlang = data.termlang local usex = data.usex local sc = data.sc local sc_explicit = sc local tr = data.tr local ts = data.ts local norm = data.norm local normsc = data.normsc local subst = data.subst local quote = data.quote local leftq = data.q local rightq = data.qq local ref = data.ref local nocat = data.nocat local categories = data.categories local example_type = data.example_type local title = data.title if normsc == "auto" then normsc = nil elseif not normsc then normsc = sc end if not sc then sc = lang:findBestScript(usex) end if not normsc and norm then normsc = lang:findBestScript(norm) end local langcode = lang:getFullCode() -- tr=- means omit transliteration altogether if tr == "-" then tr = nil else -- Try to auto-transliterate. if tr then -- [[Special:WhatLinksHere/Wiktionary:Tracking/usex/manual-tr]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/usex/manual-tr/LANGCODE]] track("manual-tr", langcode) else -- First, try transliterating the normalization, if supplied. if norm and normsc then local normsc_code = normsc:getCode() if normsc_code ~= "None" and not normsc_code:find("Lat") then -- Latn, Latf, Latg, pjt-Latn local subbed_norm = export.apply_subst(norm, subst, track) tr = lang:transliterate(subbed_norm, normsc) end end -- If no normalization, or the normalization is in a Latin script, or the transliteration of the -- normalization failed, fall back to transliterating the usex. if not tr and usex then local subbed_usex = export.apply_subst(usex, subst, track) tr = lang:transliterate(subbed_usex, sc) end -- If the language doesn't have capitalization and is specified in [[Module:usex/data]], then capitalize any sentences. -- Exclamation marks and question marks need to be unescaped then re-escaped. if tr and mw.loadData(usex_data_module).capitalize_sentences[langcode] then tr = tr:gsub("&#x21;", "!") :gsub("&#x3F;", "?") tr = rsub(tr, "%f[^%z%p%s](.)(.-[%.%?!‽])", function(m1, m2) return uupper(m1) .. m2 end) tr = tr:gsub("!", "&#x21;") :gsub("%?", "&#x3F;") end end -- If there is still no transliteration, then add a cleanup category. if not tr and needs_translit[langcode] then local sccode = sc:getCode() if sccode ~= "None" and not sccode:find("Lat") then table.insert(categories, ("Requests for transliteration of %s %ss"):format(lang:getCanonicalName(), example_type)) end end end if tr and norm then track("tr-and-norm") end if tr then tr = require(script_utilities_module).tag_translit(tr, langcode, "usex") end if ts then ts = require(script_utilities_module).tag_transcription(ts, langcode, "usex") ts = "/" .. ts .. "/" end local function do_language_and_script_tagging(usex, lang, sc, css_class) usex = require(links_module).embedded_language_links{term = usex, lang = lang, sc = sc} local face if quote then face = nil else face = "term" end usex = require(script_utilities_module).tag_text(usex, lang, sc, face, css_class) return usex end if usex then usex = do_language_and_script_tagging(usex, lang, sc, quote == "quote-meta" and css_classes.quotation_with_citation or quote and css_classes.quotation or css_classes.example) if not nocat then -- Only add [[Citations:foo]] to [[:Category:LANG terms with quotations]] if [[foo]] exists. local ok_to_add_cat if title.nsText ~= "Citations" then ok_to_add_cat = true else -- Here we don't want to use the subpage text because we check [[Citations:foo]] against [[foo]] and -- if there's a slash in what follows 'Citations:', we want to check against the full page with the -- slash. local mainspace_title = mw.title.new(title.text) if mainspace_title and mainspace_title.exists then ok_to_add_cat = true end end if ok_to_add_cat then -- Categories beginning with the language name should use full languages as that's what the poscat -- system requires, but 'Requests for' categories can use etymology-only languages. table.insert(categories, ("%s terms with %ss"):format(termlang:getFullName(), example_type)) end end else if tr then table.insert(categories, ("Requests for %s in %s %ss"):format( sc_explicit and sc_explicit:getDisplayForm() or "native script", lang:getCanonicalName(), example_type)) end -- TODO: Trigger some kind of error here usex = "<small>(please add the primary text of this " .. example_type .. ")</small>" end if norm then -- Use brackets in HTML entity format just to make sure we don't interfere with links; add brackets before -- script tagging so that if the script tagging increases the font size, the brackets get increased too. norm = "&#91;" .. norm .. "&#93;" norm = do_language_and_script_tagging(norm, lang, normsc, css_classes.normalization) end local result = {} if leftq and #leftq > 0 then table.insert(result, span(css_classes.qualifier, require("Module:qualifier").format_qualifier(leftq)) .. " ") end table.insert(result, usex) if rightq and #rightq > 0 then table.insert(result, " " .. span(css_classes.qualifier, require("Module:qualifier").format_qualifier(rightq))) end if ref and ref ~= "" then track("ref") table.insert(result, ref) end return { usex = table.concat(result), tr = tr, ts = ts, norm = norm } end local function format_audio(audio) if audio then return " [[File:" .. audio .. "|25px]]" else return "" end end --[==[ Format a usex or quotation. Implementation of {{tl|ux}}, {{tl|quote}} and {{tl|quote-*}} templates (e.g. {{tl|quote-book}}, {{tl|quote-journal}}, {{tl|quote-web}}, etc.). FIXME: Should also be used by {{tl|Q}} and [[Module:Quotations]]. Takes a single object `data`, containining the following fields: * `usex`: The text of the usex or quotation to format. Semi-mandatory (a maintenance line is displayed if missing). * `lang`: The language object of the text. Mandatory. May be an etymology language. * `termlang`: The language object of the term, which may be different from the language of the text. Defaults to `lang`. Used for categories. May be an etymology language. * `sc`: The script object of the text. Autodetected if not given. * `quote`: If specified, this is a quotation rather than a usex (uses a different CSS class that affects formatting). * `inline`: If specified, format the usex or quotation inline (on one line). * `translation`: Translation of the usex or quotation, if in a foreign language. * `lit`: Literal translation (if the translation in `translation` is idiomatic and differs significantly from the literal translation). * `normalization`: Normalized version of the usex or quotation (esp. for older languages where nonstandard spellings were common). * `normsc`: Script object of the normalized text. If unspecified, use the script object given in `sc` if any, otherwise do script detection on the normalized text. If "auto", do script detection on the normalized text even if a script was specified in `sc`. * `transliteration`: Transliteration of the usex. If unspecified, transliterate the normalization if specified and not in a Latin script and transliterable, otherwise fall back to transliterating the usex text. * `transcription`: Transcription of the usex, for languages where the transliteration differs significantly from the pronunciation. * `subst`: String indicating substitutions to perform on the usex/quotation and normalization prior to transliterating them. Multiple substs are comma-separated and individual substs are of the form FROM//TO where FROM is a Lua pattern and TO is a Lua replacement spec. (FROM/TO is also recognized if no // is present in the substitution.) * `q`: If specified, a list of left qualifiers to display before the usex/quotation text. * `qq`: If specified, a list of right qualifiers to display after the usex/quotation text. * `qualifiers`: If specified, a list of right qualifiers to display after the usex/quotation text, for compatibility purposes. * `ref`: Reference text to display directly after the right qualifiers. (FIXME: Instead, this should be actual references.) * `audio`: Name of the audio file containing the usex in spoken form. * `orig`: Original text, if the primary text of the usex or quotation is a translation. * `origlang`: The language object of the original text. Mandatory if original text given. May be an etymology language. * `origsc`: The script object of the original text. Autodetected if not given. * `orignorm`: Normalized version of the original text (esp. for older languages where nonstandard spellings were common). * `orignormsc`: Script object of the normalized original text. If unspecified, use the script object given in `origsc` if any, otherwise do script detection on the normalized original text. If "auto", do script detection on the normalized text even if a script was specified in `origsc`. * `origtr`: Transliteration of the original text. If unspecified, transliterate the normalized original text if specified and not in a Latin script and transliterable, otherwise fall back to transliterating the original text. * `origts`: Transcription of the original text, for languages where the transliteration differs significantly from the pronunciation. * `origsubst`: String indicating substitutions to perform on the original text and normalization thereof prior to transliterating them. Multiple substs are comma-separated and individual substs are of the form FROM//TO where FROM is a Lua pattern and TO is a Lua replacement spec. (FROM/TO is also recognized if no // is present in the substitution.) * `origq`: If specified, a list of left qualifiers to display before the original text. * `origqq`: If specified, a list of right qualifiers to display after the original text. * `origref`: Reference text to display directly after the right qualifiers of the original text. (FIXME: Instead, this should be actual references.) * `source`: Source of the quotation, displayed in parens after the quotation text. * `footer`: Footer displaying miscellaneous information, shown after the quotation. (Typically this should be in a small font.) * `nocat`: Suppress categorization. * `noreq`: Suppress request for translation when no translation provided. * `sortkey`: Sort key for categories. * `brackets`: If specified, show a bracket at the end (used with brackets= in {{tl|quote-*}} templates, which show the bracket at the beginning, to indicate a mention rather than a use). * `class`: Additional CSS class surrounding the entire formatted text. ]==] function export.format_usex(data) local lang = data.lang local termlang = data.termlang or lang local translation = data.translation local quote = data.quote local lit = data.lit local audio = data.audio local source = data.source local brackets = data.brackets local footer = data.footer local sortkey = data.sortkey local noreq = data.noreq local title if data.pagename then -- for testing, doc pages, etc. title = mw.title.new(data.pagename) if not title then error(("Bad value for `data.pagename`: '%s'"):format(data.pagename)) end else title = mw.title.getCurrentTitle() end --[[ if title.nsText == "Reconstruction" or lang:hasType("reconstructed") then error("Reconstructed languages and reconstructed terms cannot have usage examples, as we have no record of their use.") end ]] if lit then lit = "(literally, “" .. span(css_classes.literally, lit) .. "”)" end if source then source = "(" .. span(css_classes.source, source) .. ")" end if footer then footer = span(css_classes.footer, footer) end local example_type = quote and "quotation" or "usage example" -- used in error messages and categories local categories = {} local usex_obj = process_usex_text { lang = lang, termlang = termlang, usex = data.usex, sc = data.sc, tr = data.transliteration, ts = data.transcription, norm = data.normalization, normsc = data.normsc, subst = data.subst, quote = data.quote, title = title, q = data.q, qq = data.qq, ref = data.ref, nocat = data.nocat, categories = categories, example_type = example_type, } local orig_obj = data.orig and process_usex_text { lang = data.origlang, -- Any categories derived from the original text should use the language of the main text or the term inside it, -- not the language of the original text. termlang = termlang, usex = data.orig, sc = data.origsc, tr = data.origtr, ts = data.origts, norm = data.orignorm, normsc = data.orignormsc, subst = data.origsubst, quote = data.quote, title = title, q = data.origq, qq = data.origqq, ref = data.origref, nocat = data.nocat, categories = categories, example_type = example_type, } or nil if translation == "-" then translation = nil table.insert(categories, ("%s %ss with omitted translation"):format(lang:getFullName(), example_type)) elseif translation then translation = span(css_classes.translation, translation) elseif not noreq then local langcode = lang:getFullCode() local origlangcode = data.origlang and data.origlang:getFullCode() if langcode ~= "en" and langcode ~= "mul" and langcode ~= "und" and origlangcode ~= "en" then -- add trreq category if translation is unspecified and language is not english, translingual or -- undetermined table.insert(categories, ("Requests for translations of %s %ss"):format(lang:getCanonicalName(), example_type)) if quote then translation = "<small>(please [[WT:Quotations#Adding translations to quotations|add an English translation]] of this " .. example_type .. ")</small>" else translation = "<small>(please add an English translation of this " .. example_type .. ")</small>" end end end local function generate_inline_usex() local result = {} local function ins(text) table.insert(result, text) end ins(usex_obj.usex) ins(format_audio(audio)) local function insert_annotations(obj) if obj.norm then ins(" " .. obj.norm) end if obj.tr or obj.ts then ins(" ―") if obj.tr then ins(" " .. obj.tr) end if obj.ts then ins(" " .. obj.ts) end end end insert_annotations(usex_obj) if orig_obj then ins(" (") ins("[" .. original_text .. orig_obj.usex .. "]") insert_annotations(orig_obj) ins(")") end if translation then ins(" ― " .. translation) end if lit then ins(" " .. lit) end if source then ins(" " .. source) end if footer then ins(" " .. footer) end if data.brackets then ins("]") end return table.concat(result) end local function generate_multiline_usex() local result = {} local function ins(text) table.insert(result, text) end ins(usex_obj.usex) ins(format_audio(audio)) local any_usex_annotations = usex_obj.tr or usex_obj.ts or usex_obj.norm or translation or lit local any_orig_annotations = orig_obj and (orig_obj.tr or orig_obj.ts or orig_obj.norm) if any_usex_annotations or orig_obj or source or footer then ins("<dl>") local function insert_dd(text) if text then ins("<dd>") ins(text) if data.brackets then ins(BRACKET_SUB) end ins("</dd>") end end insert_dd(usex_obj.norm) insert_dd(usex_obj.tr) insert_dd(usex_obj.ts) if orig_obj then insert_dd("[" .. original_text .. orig_obj.usex .. "]") if any_orig_annotations then ins("<dd><dl>") insert_dd(orig_obj.norm) insert_dd(orig_obj.tr) insert_dd(orig_obj.ts) ins("</dl></dd>") end end insert_dd(translation) insert_dd(lit) if source or footer then if any_usex_annotations then ins("<dd><dl>") end insert_dd(source) insert_dd(footer) if any_usex_annotations then ins("</dl></dd>") end end ins("</dl>") elseif data.brackets then ins(BRACKET_SUB) end result = table.concat(result) if data.brackets then result = result:gsub("^(.*)" .. BRACKET_SUB, "%1]"):gsub(BRACKET_SUB, "") end return result end local is_inline if data.inline == "auto" then result = generate_inline_usex() if get_character_width(convert_to_raw_text(result)) > MAX_INLINE_WIDTH then result = generate_multiline_usex() is_inline = false else is_inline = true end elseif data.inline then result = generate_inline_usex() is_inline = true else result = generate_multiline_usex() is_inline = false end local class = quote and css_classes.container_quotation or css_classes.container_ux if data.class then class = class .. " " .. data.class end result = (is_inline and span or div)(class, result) return result .. require("Module:utilities").format_categories(categories, lang, sortkey) end return export o14rj399xkfd16bbul4vgyizscnixhy Mòideal:usex/data 828 16638 86122 2026-07-16T23:32:15Z Altronic 4137 Copy from English Wiktionary 86122 Scribunto text/plain local data = {} -- Capitalize the first letter of transliterations of sentences in these languages. data.capitalize_sentences = { ["ii"] = true, ["jje"] = true, ["ko"] = true, } return data l3cwuue2ukv6uw9p3x5yb8d29fzjrbt Mòideal:string utilities 828 16639 86123 2026-07-16T23:33:20Z Altronic 4137 Copy from English Wiktionary 86123 Scribunto text/plain local export = {} local function_module = "Module:fun" local load_module = "Module:load" local memoize_module = "Module:memoize" local string_char_module = "Module:string/char" local string_charset_escape_module = "Module:string/charsetEscape" local mw = mw local string = string local table = table local ustring = mw.ustring local byte = string.byte local char = string.char local concat = table.concat local find = string.find local format = string.format local gmatch = string.gmatch local gsub = string.gsub local insert = table.insert local len = string.len local lower = string.lower local match = string.match local next = next local require = require local reverse = string.reverse local select = select local sort = table.sort local sub = string.sub local tonumber = tonumber local tostring = tostring local type = type local ucodepoint = ustring.codepoint local ufind = ustring.find local ugcodepoint = ustring.gcodepoint local ugmatch = ustring.gmatch local ugsub = ustring.gsub local ulower = ustring.lower local umatch = ustring.match local unpack = unpack or table.unpack -- Lua 5.2 compatibility local upper = string.upper local usub = ustring.sub local uupper = ustring.upper local memoize = require(memoize_module) -- Defined below. local codepoint local explode_utf8 local format_fun local get_charset local gsplit local pattern_escape local pattern_simplifier local replacement_escape local title_case local trim local ucfirst local ulen --[==[ 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 charset_escape(...) charset_escape = require(string_charset_escape_module) return charset_escape(...) end local function is_callable(...) is_callable = require(function_module).is_callable return is_callable(...) end local function load_data(...) load_data = require(load_module).load_data return load_data(...) end local function u(...) u = require(string_char_module) return u(...) end local function prepare_iter(str, pattern, str_lib, plain) local callable = is_callable(pattern) if str_lib or plain then return pattern, #str, string, callable elseif not callable then local simple = pattern_simplifier(pattern) if simple then return simple, #str, string, false end end return pattern, ulen(str), ustring, callable end --[==[ Returns {nil} if the input value is the empty string, or otherwise the same value. If the input is a string and `do_trim` is set, the input value will be trimmed before returning; if the trimmed value is the empty string, returns {nil}. If `quote_delimiters` is set, then any outer pair of quotation marks ({' '} or {" "}) surrounding the rest of the input string will be stripped, if present. The string will not be trimmed again, converted to {nil}, or have further quotation marks stripped, as it exists as a way to embed spaces or the empty string in an input. Genuine quotation marks may also be embedded this way (e.g. {"''foo''"} returns {"'foo'"}). ]==] function export.is_not_empty(str, do_trim, quote_delimiters) if str == "" then return nil elseif not (str and type(str) == "string") then return str elseif do_trim then str = trim(str) if str == "" then return nil end end return quote_delimiters and gsub(str, "^(['\"])(.*)%1$", "%2") or str end --[==[ Explodes a string into an array of UTF-8 characters. '''Warning''': this function assumes that the input is valid UTF-8 in order to optimize speed and memory use. Passing in an input containing non-UTF-8 byte sequences could result in unexpected behaviour. ]==] function export.explode_utf8(str) local text, i = {}, 0 for ch in gmatch(str, ".[\128-\191]*") do i = i + 1 text[i] = ch end return text end explode_utf8 = export.explode_utf8 --[==[ Returns {true} if `str` is a valid UTF-8 string. This is true if, for each character, all of the following are true: * It has the expected number of bytes, which is determined by value of the leading byte: 1-byte characters are `0x00` to `0x7F`, 2-byte characters start with `0xC2` to `0xDF`, 3-byte characters start with `0xE0` to `0xEF`, and 4-byte characters start with `0xF0` to `0xF4`. * The leading byte must not fall outside of the above ranges. * The trailing byte(s) (if any), must be between `0x80` to `0xBF`. * The character's codepoint must be between U+0000 (`0x00`) and U+10FFFF (`0xF4 0x8F 0xBF 0xBF`). * The character cannot have an overlong encoding: for each byte length, the lowest theoretical encoding is equivalent to U+0000 (e.g. `0xE0 0x80 0x80`, the lowest theoretical 3-byte encoding, is exactly equivalent to U+0000). Encodings that use more than the minimum number of bytes are not considered valid, meaning that the first valid 3-byte character is `0xE0 0xA0 0x80` (U+0800), and the first valid 4-byte character is `0xF0 0x90 0x80 0x80` (U+10000). Formally, 2-byte characters have leading bytes ranging from `0xC0` to `0xDF` (rather than `0xC2` to `0xDF`), but `0xC0 0x80` to `0xC1 0xBF` are overlong encodings, so it is simpler to say that the 2-byte range begins at `0xC2`. If `allow_surrogates` is set, surrogates (U+D800 to U+DFFF) will be treated as valid UTF-8. Surrogates are used in UTF-16, which encodes codepoints U+0000 to U+FFFF with 2 bytes, and codepoints from U+10000 upwards using a pair of surrogates, which are taken together as a 4-byte unit. Since surrogates have no use in UTF-8, as it encodes higher codepoints in a different way, they are not considered valid in UTF-8 text. However, there are limited circumstances where they may be necessary: for instance, JSON escapes characters using the format `\u0000`, which must contain exactly 4 hexadecimal digits; under the scheme, codepoints above U+FFFF must be escaped as the equivalent pair of surrogates, even though the text itself must be encoded in UTF-8 (e.g. U+10000 becomes `\uD800\uDC00`). ]==] function export.isutf8(str, allow_surrogates) for ch in gmatch(str, "[\128-\255][\128-\191]*") do if #ch > 4 then return false end local b1, b2, b3, b4 = byte(ch, 1, 4) if not (b2 and b2 >= 0x80 and b2 <= 0xBF) then return false -- 1-byte is always invalid, as gmatch excludes 0x00 to 0x7F elseif not b3 then -- 2-byte if not (b1 >= 0xC2 and b1 <= 0xDF) then -- b1 == 0xC0 or b1 == 0xC1 is overlong return false end elseif not (b3 >= 0x80 and b3 <= 0xBF) then -- trailing byte return false elseif not b4 then -- 3-byte if b1 > 0xEF then return false elseif b2 < 0xA0 then if b1 < 0xE1 then -- b1 == 0xE0 and b2 < 0xA0 is overlong return false end elseif b1 < 0xE0 or (b1 == 0xED and not allow_surrogates) then -- b1 == 0xED and b2 >= 0xA0 is a surrogate return false end elseif not (b4 >= 0x80 and b4 <= 0xBF) then -- 4-byte return false elseif b2 < 0x90 then if not (b1 >= 0xF1 and b1 <= 0xF4) then -- b1 == 0xF0 and b2 < 0x90 is overlong return false end elseif not (b1 >= 0xF0 and b1 <= 0xF3) then -- b1 == 0xF4 and b2 >= 0x90 is too high return false end end return true end do local charset_chars = { ["\0"] = "%z", ["%"] = "%%", ["-"] = "%-", ["]"] = "%]", ["^"] = "%^" } charset_chars.__index = charset_chars local chars = setmetatable({ ["$"] = "%$", ["("] = "%(", [")"] = "%)", ["*"] = "%*", ["+"] = "%+", ["."] = "%.", ["?"] = "%?", ["["] = "%[" }, charset_chars) --[==[ Escapes the magic characters used in a [[mw:Extension:Scribunto/Lua reference manual#Patterns|pattern]] (Lua's version of regular expressions): {$%()*+-.?[]^}, and converts the null character to {%z}. For example, {"^$()%.[]*+-?\0"} becomes {"%^%$%(%)%%%.%[%]%*%+%-%?%z"}. This is necessary when constructing a pattern involving arbitrary text (e.g. from user input). ]==] function export.pattern_escape(str) return (gsub(str, "[%z$%%()*+%-.?[%]^]", chars)) end pattern_escape = export.pattern_escape --[==[ Escapes only {%}, which is the only magic character used in replacement [[mw:Extension:Scribunto/Lua reference manual#Patterns|patterns]] with string.gsub and mw.ustring.gsub. ]==] function export.replacement_escape(str) return (gsub(str, "%%", "%%%%")) end replacement_escape = export.replacement_escape local function case_insensitive_char(ch) local upper_ch = uupper(ch) if upper_ch == ch then ch = ulower(ch) if ch == upper_ch then return chars[ch] or ch end end return "[" .. (charset_chars[upper_ch] or upper_ch) .. (charset_chars[ch] or ch) .. "]" end local function iterate(str, str_len, text, n, start, _gsub, _sub, loc1, loc2) if not (loc1 and start <= str_len) then -- Add final chunk and return. n = n + 1 text[n] = _gsub(_sub(str, start), ".", chars) return elseif loc2 < loc1 then if _sub == sub then local b = byte(str, loc1) if b and b >= 128 then loc1 = loc1 + (b < 224 and 1 or b < 240 and 2 or 3) end end n = n + 1 text[n] = _gsub(_sub(str, start, loc1), ".", chars) start = loc1 + 1 if start > str_len then return end else -- Add chunk up to the current match. n = n + 1 text[n] = _gsub(_sub(str, start, loc1 - 1), ".", chars) -- Add current match. n = n + 1 text[n] = _gsub(_sub(str, loc1, loc2), ".", case_insensitive_char) start = loc2 + 1 end return n, start end --[==[ Escapes the magic characters used in a [[mw:Extension:Scribunto/Lua reference manual#Patterns|pattern]], and makes all characters case-insensitive. An optional pattern or find function (see {split}) may be supplied as the second argument, the third argument (`str_lib`) forces use of the string library, while the fourth argument (`plain`) turns any pattern matching facilities off in the optional pattern supplied. ]==] function export.case_insensitive_pattern(str, pattern_or_func, str_lib, plain) if pattern_or_func == nil then return (gsub(str, str_lib and "[^\128-\255]" or ".[\128-\191]*", case_insensitive_char)) end local text, n, start, str_len, _string, callable = {}, 0, 1 pattern_or_func, str_len, _string, callable = prepare_iter(str, pattern_or_func, str_lib, plain) local _find, _gsub, _sub = _string.find, _string.gsub, _string.sub if callable then repeat n, start = iterate(str, str_len, text, n, start, _gsub, _sub, pattern_or_func(str, start)) until not start -- Special case if the pattern is anchored to the start: "^" always -- anchors to the start position, not the start of the string, so get -- around this by only attempting one match with the pattern, then match -- the end of the string. elseif byte(pattern_or_func) == 0x5E then -- ^ n, start = iterate(str, str_len, text, n, start, _gsub, _sub, _find(str, pattern_or_func, start, plain)) if start ~= nil then iterate(str, str_len, text, n, start, _gsub, _sub, _find(str, "$", start, plain)) end else repeat n, start = iterate(str, str_len, text, n, start, _gsub, _sub, _find(str, pattern_or_func, start, plain)) until not start end return concat(text) end end do local character_classes local function get_character_classes() character_classes, get_character_classes = { [0x41] = true, [0x61] = true, -- Aa [0x43] = true, [0x63] = true, -- Cc [0x44] = true, [0x64] = true, -- Dd [0x4C] = true, [0x6C] = true, -- Ll [0x50] = true, [0x70] = true, -- Pp [0x53] = true, [0x73] = true, -- Ss [0x55] = true, [0x75] = true, -- Uu [0x57] = true, [0x77] = true, -- Ww [0x58] = true, [0x78] = true, -- Xx [0x5A] = true, -- z dealt with separately. }, nil return character_classes end local function check_sets_equal(set1, set2) local k2 for k1, v1 in next, set1 do local v2 = set2[k1] if v1 ~= v2 and (v2 == nil or not check_sets_equal(v1, v2)) then return false end k2 = next(set2, k2) end return next(set2, k2) == nil end local function check_sets(bytes) local key, set1, set = next(bytes) if set1 == true then return true elseif not check_sets(set1) then return false end while true do key, set = next(bytes, key) if not key then return true elseif not check_sets_equal(set, set1) then return false end end end local function make_charset(range) if #range == 1 then return char(range[1]) end sort(range) local compressed, n, start = {}, 0, range[1] for i = 1, #range do local this, nxt = range[i], range[i + 1] if nxt ~= this + 1 then n = n + 1 compressed[n] = this == start and char(this) or char(start) .. "-" .. char(this) start = nxt end end return "[" .. concat(compressed) .. "]" end local function parse_1_byte_charset(pattern, pos) local ch while true do pos, ch = match(pattern, "()([%%%]\192-\255])", pos) if ch == "%" then local nxt = byte(pattern, pos + 1) if not nxt or nxt >= 128 or (character_classes or get_character_classes())[nxt] then -- acdlpsuwxACDLPSUWXZ, but not z return false end pos = pos + 2 elseif ch == "]" then pos = pos + 1 return pos else return false end end end --[==[ Parses `pattern`, a ustring library pattern, and attempts to convert it into a string library pattern. If conversion isn't possible, returns false. ]==] function pattern_simplifier(pattern) if type(pattern) == "number" then return tostring(pattern) end local pos, capture_groups, start, n, output, ch, nxt_pos = 1, 0, 1, 0 while true do -- FIXME: use "()([%%(.[\128-\255])[\128-\191]?[\128-\191]?[\128-\191]?()" and ensure non-UTF8 always fails. pos, ch, nxt_pos = match(pattern, "()([%%(.[\192-\255])[\128-\191]*()", pos) if not ch then break end local nxt = byte(pattern, nxt_pos) if ch == "%" then if nxt == 0x62 then -- b local nxt2, nxt3 = byte(pattern, pos + 2, pos + 3) if not (nxt2 and nxt2 < 128 and nxt3 and nxt3 < 128) then return false end pos = pos + 4 elseif nxt == 0x66 then -- f nxt_pos = nxt_pos + 2 local nxt2, nxt3 = byte(pattern, nxt_pos - 1, nxt_pos) -- Only possible to convert a positive %f charset which is -- all ASCII, so use parse_1_byte_charset. if not (nxt2 == 0x5B and nxt3 and nxt3 ~= 0x5E and nxt3 < 128) then -- [^ return false elseif nxt3 == 0x5D then -- Initial ] is non-magic. nxt_pos = nxt_pos + 1 end pos = parse_1_byte_charset(pattern, nxt_pos) if not pos then return false end elseif nxt == 0x5A then -- Z nxt = byte(pattern, nxt_pos + 1) if nxt == 0x2A or nxt == 0x2D then -- *- pos = pos + 3 else if output == nil then output = {} end local ins = sub(pattern, start, pos - 1) .. "[\1-\127\192-\255]" n = n + 1 if nxt == 0x2B then -- + output[n] = ins .. "%Z*" pos = pos + 3 elseif nxt == 0x3F then -- ? output[n] = ins .. "?[\128-\191]*" pos = pos + 3 else output[n] = ins .. "[\128-\191]*" pos = pos + 2 end start = pos end elseif not nxt or (character_classes or get_character_classes())[nxt] then -- acdlpsuwxACDLPSUWX, but not Zz return false -- Skip the next character if it's ASCII. Otherwise, we will -- still need to do length checks. else pos = pos + (nxt < 128 and 2 or 1) end elseif ch == "(" then if nxt == 0x29 or capture_groups == 32 then -- ) return false end capture_groups = capture_groups + 1 pos = pos + 1 elseif ch == "." then if nxt == 0x2A or nxt == 0x2D then -- *- pos = pos + 2 else if output == nil then output = {} end local ins = sub(pattern, start, pos - 1) .. "[^\128-\191]" n = n + 1 if nxt == 0x2B then -- + output[n] = ins .. ".*" pos = pos + 2 elseif nxt == 0x3F then -- ? output[n] = ins .. "?[\128-\191]*" pos = pos + 2 else output[n] = ins .. "[\128-\191]*" pos = pos + 1 end start = pos end elseif ch == "[" then -- Fail negative charsets. TODO: 1-byte charsets should be safe. if nxt == 0x5E then -- ^ return false -- If the first character is "%", ch_len is determined by the -- next one instead. elseif nxt == 0x25 then -- % nxt = byte(pattern, nxt_pos + 1) elseif nxt == 0x5D then -- Initial ] is non-magic. nxt_pos = nxt_pos + 1 end if not nxt then return false end local ch_len = nxt < 128 and 1 or nxt < 224 and 2 or nxt < 240 and 3 or 4 if ch_len == 1 then -- Single-byte charset. pos = parse_1_byte_charset(pattern, nxt_pos) if not pos then return false end else -- Multibyte charset. -- TODO: 1-byte chars should be safe to mix with multibyte chars. CONFIRM THIS FIRST. local charset_pos, bytes = pos pos = pos + 1 while true do -- TODO: non-ASCII charset ranges. pos, ch, nxt_pos = match(pattern, "^()([^\128-\191])[\128-\191]*()", pos) -- If escaped, get the next character. No need to -- distinguish magic characters or character classes, -- as they'll all fail for having the wrong length -- anyway. if ch == "%" then pos, ch, nxt_pos = match(pattern, "^()([^\128-\191])[\128-\191]*()", nxt_pos) elseif ch == "]" then pos = nxt_pos break end if not (ch and nxt_pos - pos == ch_len) then return false elseif bytes == nil then bytes = {} end local bytes, last = bytes, nxt_pos - 1 for i = pos, last - 1 do local b = byte(pattern, i) local bytes_b = bytes[b] if bytes_b == nil then bytes_b = {} bytes[b] = bytes_b end bytes[b], bytes = bytes_b, bytes_b end bytes[byte(pattern, last)] = true pos = nxt_pos end if not pos then return false end nxt = byte(pattern, pos) if ( (nxt == 0x2A or nxt == 0x2D or nxt == 0x3F) or -- *-? (nxt == 0x2B and ch_len > 2) or -- + not check_sets(bytes) ) then return false end local ranges, b, key, next_byte = {}, 0 repeat key, next_byte = next(bytes) local range, n = {key}, 1 -- Loop starts on the second iteration. for key in next, bytes, key do n = n + 1 range[n] = key end b = b + 1 ranges[b] = range bytes = next_byte until next_byte == true if nxt == 0x2B then -- + local range1, range2 = ranges[1], ranges[2] ranges[1], ranges[3] = make_charset(range1), make_charset(range2) local n = #range2 for i = 1, #range1 do n = n + 1 range2[n] = range1[i] end ranges[2] = make_charset(range2) .. "*" pos = pos + 1 else for i = 1, #ranges do ranges[i] = make_charset(ranges[i]) end end if output == nil then output = {} end nxt = byte(pattern, pos) n = n + 1 output[n] = sub(pattern, start, charset_pos - 1) .. concat(ranges) .. ((nxt == 0x2A or nxt == 0x2B or nxt == 0x2D or nxt == 0x3F) and "%" or "") -- following *+-? now have to be escaped start = pos end elseif not nxt then break elseif nxt == 0x2B then -- + if nxt_pos - pos ~= 2 then return false elseif output == nil then output = {} end pos, nxt_pos = pos + 1, nxt_pos + 1 nxt = byte(pattern, nxt_pos) local ch2 = sub(pattern, pos, pos) n = n + 1 output[n] = sub(pattern, start, pos - 1) .. "[" .. ch .. ch2 .. "]*" .. ch2 .. ((nxt == 0x2A or nxt == 0x2B or nxt == 0x2D or nxt == 0x3F) and "%" or "") -- following *+-? now have to be escaped pos, start = nxt_pos, nxt_pos elseif nxt == 0x2A or nxt == 0x2D or nxt == 0x3F then -- *-? return false else pos = nxt_pos end end if start == 1 then return pattern end return concat(output) .. sub(pattern, start) end pattern_simplifier = memoize(pattern_simplifier, true) export.pattern_simplifier = pattern_simplifier end --[==[ Parses `charset`, the interior of a string or ustring library character set, and normalizes it into a string or ustring library pattern (e.g. {"abcd-g"} becomes {"[abcd-g]"}, and {"[]"} becomes {"[[%]]"}). The negative (`^`), range (`-`) and literal (`%`) magic characters work as normal, and character classes may be used (e.g. `%d` and `%w`), but opening and closing square brackets are sanitized so that they behave like ordinary characters. ]==] function get_charset(charset) if type(charset) == "number" then return tostring(charset) end local pos, start, n, output = 1, 1, 0 if byte(charset) == 0x5E then -- ^ pos = pos + 1 end -- FIXME: "]" is non-magic if it's the first character in a charset. local nxt_pos, nxt while true do local new_pos, ch = match(charset, "()([%%%-%]])", pos) if not ch then break -- Skip percent escapes. Ranges can't start with them, either. elseif ch == "%" then pos = new_pos + 2 else -- If `ch` is a hyphen, get the character before iff it's at or ahead of `pos`. if ch == "-" and new_pos > pos then pos, nxt_pos, nxt = new_pos - 1, new_pos, ch ch = sub(charset, pos, pos) else pos, nxt_pos = new_pos, new_pos + 1 nxt = sub(charset, nxt_pos, nxt_pos) end -- Range. if nxt == "-" then if output == nil then output = {} end n = n + 1 output[n] = sub(charset, start, pos - 1) nxt_pos = nxt_pos + 1 nxt = sub(charset, nxt_pos, nxt_pos) -- Ranges fail if they end with a percent escape, so escape the hyphen to avoid undefined behaviour. if nxt == "" or nxt == "%" then n = n + 1 output[n] = (ch == "]" and "%]" or ch) .. "%-" start = nxt_pos nxt_pos = nxt_pos + 2 -- Since ranges can't contain "%]", since it's escaped, range inputs like "]-z" or "a-]" must be -- adjusted to the character before or after, plus "%]" (e.g. "%]^-z" or "a-\\%]"). The escaped "%]" is -- omitted if the range would be empty (i.e. if the first byte is greater than the second). else n = n + 1 output[n] = (ch == "]" and (byte(nxt) >= 0x5D and "%]^" or "^") or ch) .. "-" .. (nxt == "]" and (byte(ch) <= 0x5D and "\\%]" or "\\") or nxt) nxt_pos = nxt_pos + 1 start = nxt_pos end elseif ch == "-" or ch == "]" then if output == nil then output = {} end n = n + 1 output[n] = sub(charset, start, pos - 1) .. "%" .. ch start = nxt_pos end pos = nxt_pos end end if start == 1 then return "[" .. charset .. "]" end return "[" .. concat(output) .. sub(charset, start) .. "]" end get_charset = memoize(get_charset, true) export.get_charset = get_charset function export.len(str) return type(str) == "number" and len(str) or #str - #gsub(str, "[^\128-\191]+", "") end ulen = export.len function export.sub(str, i, j) str, i = type(str) == "number" and tostring(str) or str, i or 1 if i < 0 or j and j < 0 then return usub(str, i, j) elseif j and i > j or i > #str then return "" end local n, new_i = 0 for loc1, loc2 in gmatch(str, "()[^\128-\191]+()[\128-\191]*") do n = n + loc2 - loc1 if not new_i and n >= i then new_i = loc2 - (n - i) - 1 if not j then return sub(str, new_i) end end if j and n > j then return sub(str, new_i, loc2 - (n - j) - 1) end end return new_i and sub(str, new_i) or "" end do local function _find(str, loc1, loc2, ...) if loc1 and not match(str, "^()[^\128-\255]*$") then -- Use raw values of loc1 and loc2 to get loc1 and the length of the match. loc1, loc2 = ulen(sub(str, 1, loc1)), ulen(sub(str, loc1, loc2)) -- Offset length with loc1 to get loc2. loc2 = loc1 + loc2 - 1 end return loc1, loc2, ... end --[==[A version of find which uses string.find when possible, but otherwise uses mw.ustring.find.]==] function export.find(str, pattern, init, plain) init = init or 1 if init ~= 1 and not match(str, "^()[^\128-\255]*$") then return ufind(str, pattern, init, plain) elseif plain then return _find(str, find(str, pattern, init, true)) end local simple = pattern_simplifier(pattern) if simple then return _find(str, find(str, simple, init)) end return ufind(str, pattern, init) end end --[==[A version of match which uses string.match when possible, but otherwise uses mw.ustring.match.]==] function export.match(str, pattern, init) init = init or 1 if init ~= 1 and not match(str, "^()[^\128-\255]*$") then return umatch(str, pattern, init) end local simple = pattern_simplifier(pattern) if simple then return match(str, simple, init) end return umatch(str, pattern, init) end --[==[A version of gmatch which uses string.gmatch when possible, but otherwise uses mw.ustring.gmatch.]==] function export.gmatch(str, pattern) local simple = pattern_simplifier(pattern) if simple then return gmatch(str, simple) end return ugmatch(str, pattern) end --[==[A version of gsub which uses string.gsub when possible, but otherwise uses mw.ustring.gsub.]==] function export.gsub(str, pattern, repl, n) local simple = pattern_simplifier(pattern) if simple then return gsub(str, simple, repl, n) end return ugsub(str, pattern, repl, n) end --[==[ Like gsub, but pattern-matching facilities are turned off, so `pattern` and `repl` (if a string) are treated as literal. ]==] function export.plain_gsub(str, pattern, repl, n) return gsub(str, pattern_escape(pattern), type(repl) == "string" and replacement_escape(repl) or repl, n) end --[==[ Reverses a UTF-8 string; equivalent to string.reverse. ]==] function export.reverse(str) return reverse((gsub(str, "[\192-\255][\128-\191]*", reverse))) end function export.char(...) -- To be moved to [[Module:string/char]]. return u(...) end do local function utf8_err(func_name) error(format("bad argument #1 to '%s' (string is not UTF-8)", func_name), 4) end local function get_codepoint(func_name, b1, b2, b3, b4) if b1 <= 0x7F then return b1, 1 elseif not (b2 and b2 >= 0x80 and b2 <= 0xBF) then utf8_err(func_name) elseif b1 <= 0xDF then local cp = 0x40 * b1 + b2 - 0x3080 return cp >= 0x80 and cp or utf8_err(func_name), 2 elseif not (b3 and b3 >= 0x80 and b3 <= 0xBF) then utf8_err(func_name) elseif b1 <= 0xEF then local cp = 0x1000 * b1 + 0x40 * b2 + b3 - 0xE2080 return cp >= 0x800 and cp or utf8_err(func_name), 3 elseif not (b4 and b4 >= 0x80 and b4 <= 0xBF) then utf8_err(func_name) end local cp = 0x40000 * b1 + 0x1000 * b2 + 0x40 * b3 + b4 - 0x3C82080 return cp >= 0x10000 and cp <= 0x10FFFF and cp or utf8_err(func_name), 4 end function export.codepoint(str, i, j) if str == "" then return -- return nothing elseif type(str) == "number" then return byte(str, i, j) end i, j = i or 1, j == -1 and #str or i or 1 if i == 1 and j == 1 then return (get_codepoint("codepoint", byte(str, 1, 4))) elseif i < 0 or j < 0 then return ucodepoint(str, i, j) -- FIXME end local n, nb, ret, nr = 0, 1, {}, 0 while n < j do n = n + 1 if n < i then local b = byte(str, nb) nb = nb + (b < 128 and 1 or b < 224 and 2 or b < 240 and 3 or 4) else local b1, b2, b3, b4 = byte(str, nb, nb + 3) if not b1 then break end nr = nr + 1 local add ret[nr], add = get_codepoint("codepoint", b1, b2, b3, b4) nb = nb + add end end return unpack(ret) end codepoint = export.codepoint function export.gcodepoint(str, i, j) i, j = i or 1, j ~= -1 and j or nil if i < 0 or j and j < 0 then return ugcodepoint(str, i, j) -- FIXME end local n, nb = 1, 1 while n < i do local b = byte(str, nb) if not b then break end nb = nb + (b < 128 and 1 or b < 224 and 2 or b < 240 and 3 or 4) n = n + 1 end return function() if j and n > j then return nil end n = n + 1 local b1, b2, b3, b4 = byte(str, nb, nb + 3) if not b1 then return nil end local ret, add = get_codepoint("gcodepoint", b1, b2, b3, b4) nb = nb + add return ret end end end do local _ulower = ulower --[==[A version of lower which uses string.lower when possible, but otherwise uses mw.ustring.lower.]==] function export.lower(str) return (match(str, "^()[^\128-\255]*$") and lower or _ulower)(str) end end do local _uupper = uupper --[==[A version of upper which uses string.upper when possible, but otherwise uses mw.ustring.upper.]==] function export.upper(str) return (match(str, "^()[^\128-\255]*$") and upper or _uupper)(str) end end do local function add_captures(t, n, ...) if ... == nil then return end -- Insert any captures from the splitting pattern. local offset, capture = n - 1, ... while capture do n = n + 1 t[n] = capture capture = select(n - offset, ...) end return n end --[==[ Reimplementation of mw.text.split() that includes any capturing groups in the splitting pattern. This works like Python's re.split() function, except that it has Lua's behavior when the split pattern is empty (i.e. advancing by one character at a time; Python returns the whole remainder of the string). When possible, it will use the string library, but otherwise uses the ustring library. There are two optional parameters: `str_lib` forces use of the string library, while `plain` turns any pattern matching facilities off, treating `pattern` as literal. In addition, `pattern` may be a custom find function (or callable table), which takes the input string and start index as its two arguments, and must return the start and end index of the match, plus any optional captures, or nil if there are no further matches. By default, the start index will be calculated using the ustring library, unless `str_lib` or `plain` is set. ]==] function export.split(str, pattern_or_func, str_lib, plain) local iter, t, n = gsplit(str, pattern_or_func, str_lib, plain), {}, 0 repeat n = add_captures(t, n, iter()) until n == nil return t end export.capturing_split = export.split -- To be removed. end --[==[ Returns an iterator function, which iterates over the substrings returned by {split}. The first value returned is the string up the splitting pattern, with any capture groups being returned as additional values on that iteration. ]==] function export.gsplit(str, pattern_or_func, str_lib, plain) local start, final, str_len, _string, callable = 1 pattern_or_func, str_len, _string, callable = prepare_iter(str, pattern_or_func, str_lib, plain) local _find, _sub = _string.find, _string.sub local function iter(loc1, loc2, ...) -- If no match, or there is but we're past the end of the string -- (which happens when the match is the empty string), then return -- the final chunk. if not loc1 then final = true return _sub(str, start) end -- Special case: If we match the empty string, then eat the -- next character; this avoids an infinite loop, and makes -- splitting by the empty string work the way mw.text.gsplit() does -- (including non-adjacent empty string matches with %f). If we -- reach the end of the string this way, set `final` to true, so we -- don't get stuck matching the empty string at the end. local chunk if loc2 < loc1 then -- If using the string library, we need to make sure we advance -- by one UTF-8 character. if _sub == sub then local b = byte(str, loc1) if b and b >= 128 then loc1 = loc1 + (b < 224 and 1 or b < 240 and 2 or 3) end end chunk = _sub(str, start, loc1) if loc1 >= str_len then final = true else start = loc1 + 1 end -- Eat chunk up to the current match. else chunk = _sub(str, start, loc1 - 1) start = loc2 + 1 end return chunk, ... end if callable then return function() if not final then return iter(pattern_or_func(str, start)) end end -- Special case if the pattern is anchored to the start: "^" always -- anchors to the start position, not the start of the string, so get -- around this by only attempting one match with the pattern, then match -- the end of the string. elseif byte(pattern_or_func) == 0x5E then -- ^ local returned return function() if not returned then returned = true return iter(_find(str, pattern_or_func, start, plain)) elseif not final then return iter(_find(str, "$", start, plain)) end end end return function() if not final then return iter(_find(str, pattern_or_func, start, plain)) end end end gsplit = export.gsplit function export.count(str, pattern, plain) if plain then return select(2, gsub(str, pattern_escape(pattern), "")) end local simple = pattern_simplifier(pattern) if simple then return select(2, gsub(str, pattern, "")) end return select(2, ugsub(str, pattern, "")) end function export.trim(str, charset, str_lib, plain) if charset == nil then -- "^.*%S" is the fastest trim algorithm except when strings only consist of characters to be trimmed, which are -- very slow due to catastrophic backtracking. gsub with "^%s*" gets around this by trimming such strings to "" -- first. return match(gsub(str, "^%s*", ""), "^.*%S") or "" elseif charset == "" then return str end charset = plain and ("[" .. charset_escape(charset) .. "]") or get_charset(charset) -- The pattern uses a non-greedy quantifier instead of the algorithm used for %s, because negative character sets -- are non-trivial to compute (e.g. "[^^-z]" becomes "[%^_-z]"). Plus, if the ustring library has to be used, there -- would be two callbacks into PHP, which is slower. local pattern = "^" .. charset .. "*(.-)" .. charset .. "*$" if not str_lib then local simple = pattern_simplifier(pattern) if not simple then return umatch(str, pattern) end pattern = simple end return match(str, pattern) end trim = export.trim do local entities local function get_entities() entities, get_entities = load_data("Module:data/entities"), nil return entities end local function decode_entity(hash, x, code) if hash == "" then return (entities or get_entities())[x .. code] end local cp if x == "" then cp = match(code, "^()%d+$") and tonumber(code) else cp = match(code, "^()%x+$") and tonumber(code, 16) end return cp and (cp <= 0xD7FF or cp >= 0xE000 and cp <= 0x10FFFF) and u(cp) or nil end -- Non-ASCII characters aren't valid in proper HTML named entities, but MediaWiki uses them in some custom aliases -- which have also been included in [[Module:data/entities]]. function export.decode_entities(str) local amp = find(str, "&", nil, true) return amp and find(str, ";", amp, true) and gsub(str, "&(#?)([xX]?)([%w\128-\255]+);", decode_entity) or str end end do local entities local function get_entities() -- Memoized HTML entities (taken from mw.text.lua). entities, get_entities = { ["\""] = "&quot;", ["&"] = "&amp;", ["'"] = "&#039;", ["<"] = "&lt;", [">"] = "&gt;", ["\194\160"] = "&nbsp;", }, nil return entities end local function encode_entity(ch) local entity = (entities or get_entities())[ch] if entity == nil then local cp = codepoint(ch) -- U+D800 to U+DFFF are surrogates, so can't be encoded as entities. entity = cp and (cp <= 0xD7FF or cp >= 0xE000) and format("&#%d;", cp) or false entities[ch] = entity end return entity or nil end function export.encode_entities(str, charset, str_lib, plain) if charset == nil then return (gsub(str, "[\"&'<>\194]\160?", entities or get_entities())) elseif charset == "" then return str end local pattern = plain and ("[" .. charset_escape(charset) .. "]") or charset == "." and charset or get_charset(charset) if not str_lib then local simple = pattern_simplifier(pattern) if not simple then return (ugsub(str, pattern, encode_entity)) end pattern = simple end return (gsub(str, pattern, encode_entity)) end end do local function decode_path(code) return char(tonumber(code, 16)) end local function decode(lead, trail) if lead == "+" or lead == "_" then return " " .. trail elseif #trail == 2 then return decode_path(trail) end return lead .. trail end function export.decode_uri(str, enctype) enctype = enctype and upper(enctype) or "QUERY" if enctype == "PATH" then return find(str, "%", nil, true) and gsub(str, "%%(%x%x)", decode_path) or str elseif enctype == "QUERY" then return (find(str, "%", nil, true) or find(str, "+", nil, true)) and gsub(str, "([%%%+])(%x?%x?)", decode) or str elseif enctype == "WIKI" then return (find(str, "%", nil, true) or find(str, "_", nil, true)) and gsub(str, "([%%_])(%x?%x?)", decode) or str end error("bad argument #2 to 'decode_uri' (expected QUERY, PATH, or WIKI)", 2) end end do local function _remove_comments(str, pre) local head = find(str, "<!--", nil, true) if not head then return str end local ret, n = {sub(str, 1, head - 1)}, 1 while true do local loc = find(str, "-->", head + 4, true) if not loc then return pre and concat(ret) or concat(ret) .. sub(str, head) end head = loc + 3 loc = find(str, "<!--", head, true) if not loc then return concat(ret) .. sub(str, head) end n = n + 1 ret[n] = sub(str, head, loc - 1) head = loc end end --[==[ Removes any HTML comments from the input text. `stage` can be one of three options: * {"PRE"} (default) applies the method used by MediaWiki's preprocessor: all {{code|html|<nowiki><!-- ... --></nowiki>}} pairs are removed, as well as any text after an unclosed {{code|html|<nowiki><!--</nowiki>}}. This is generally suitable when parsing raw template or [[mw:Parser extension tags|parser extension tag]] code. (Note, however, that the actual method used by the preprocessor is considerably more complex and differs under certain conditions (e.g. comments inside nowiki tags); if full accuracy is absolutely necessary, use [[Module:template parser]] instead). * {"POST"} applies the method used to generate the final page output once all templates have been expanded: it loops over the text, removing any {{code|html|<nowiki><!-- ... --></nowiki>}} pairs until no more are found (e.g. {{code|html|<nowiki><!-<!-- ... -->- ... --></nowiki>}} would be fully removed), but any unclosed {{code|html|<nowiki><!--</nowiki>}} is ignored. This is suitable for handling links embedded in template inputs, where the {"PRE"} method will have already been applied by the native parser. * {"BOTH"} applies {"PRE"} then {"POST"}. ]==] function export.remove_comments(str, stage) if not stage or stage == "PRE" then return _remove_comments(str, true) end local processed = stage == "POST" and _remove_comments(str) or stage == "BOTH" and _remove_comments(str, true) or error("bad argument #2 to 'remove_comments' (expected PRE, POST, or BOTH)", 2) while processed ~= str do str = processed processed = _remove_comments(str) end return str end end do local byte_escapes local function get_byte_escapes() byte_escapes, get_byte_escapes = load_data("Module:string utilities/data").byte_escapes, nil return byte_escapes end local function escape_byte(b) return (byte_escapes or get_byte_escapes())[b] or format("\\%03d", byte(b)) end function export.escape_bytes(str) return (gsub(str, ".", escape_byte)) end end function export.format_fun(str, fun) return (gsub(str, "{(\\?)((\\?)[^{}]*)}", function(p1, name, p2) if #p1 + #p2 == 1 then return name == "op" and "{" or name == "cl" and "}" or error(mw.getCurrentFrame():getTitle() .. " format: unrecognized escape sequence '{\\" .. name .. "}'") elseif fun(name) and type(fun(name)) ~= "string" then error(mw.getCurrentFrame():getTitle() .. " format: \"" .. name .. "\" is a " .. type(fun(name)) .. ", not a string") end return fun(name) or error(mw.getCurrentFrame():getTitle() .. " format: \"" .. name .. "\" not found in table") end)) end format_fun = export.format_fun --[==[ This function, unlike {string.format} and {mw.ustring.format}, takes just two parameters, a format string and a table, and replaces all instances of { {param_name} } in the format string with the table's entry for {param_name}. The opening and closing brace characters can be escaped with { {\op} } and { {\cl} }, respectively. A table entry beginning with a slash can be escaped by doubling the initial slash. ====Examples==== * {string_utilities.format("{foo} fish, {bar} fish, {baz} fish, {quux} fish", {["foo"]="one", ["bar"]="two", ["baz"]="red", ["quux"]="blue"}) } *: produces: {"one fish, two fish, red fish, blue fish"} * {string_utilities.format("The set {\\op}1, 2, 3{\\cl} contains {\\\\hello} elements.", {["\\hello"]="three"})} *: produces: {"The set {1, 2, 3} contains three elements."} *:* Note that the single and double backslashes should be entered as double and quadruple backslashes when quoted in a literal string. ]==] function export.format(str, tbl) return format_fun(str, function(key) return tbl[key] end) end do local function do_uclcfirst(str, case_func) -- Re-case the first letter. local first, remainder = match(str, "^(.[\128-\191]*)(.*)") return first and (case_func(first) .. remainder) or "" end local function uclcfirst(str, case_func) -- Strip off any HTML tags at the beginning. This currently does not handle comments or <ref>...</ref> -- correctly; it's intended for text wrapped in <span> or the like, as happens when passing text through -- [[Module:links]]. local html_at_beginning = nil if str:match("^<") then while true do local html_tag, rest = str:match("^(<.->)(.*)$") if not html_tag then break end if not html_at_beginning then html_at_beginning = {} end insert(html_at_beginning, html_tag) str = rest end end -- If there's a link at the beginning, re-case the first letter of the -- link text. This pattern matches both piped and unpiped links. -- If the link is not piped, the second capture (linktext) will be empty. local link, linktext, remainder = match(str, "^%[%[([^|%]]+)%|?(.-)%]%](.*)$") local retval if link then retval = "[[" .. link .. "|" .. do_uclcfirst(linktext ~= "" and linktext or link, case_func) .. "]]" .. remainder else retval = do_uclcfirst(str, case_func) end if html_at_beginning then retval = concat(html_at_beginning) .. retval end return retval end --[==[ Uppercase the first character of the input string, correctly handling one-part and two-part links, optionally surrounded by HTML tags such as `<nowiki><span>...</span></nowiki>`, possibly nested. Intended to correctly uppercase the first character of text that may include links that have been passed through `full_link()` in [[Module:links]] or a similar function. ]==] function export.ucfirst(str) return uclcfirst(str, uupper) end ucfirst = export.ucfirst --[==[ Lowercase the first character of the input string, correctly handling one-part and two-part links, optionally surrounded by HTML tags such as `<nowiki><span>...</span></nowiki>`, possibly nested. Intended to correctly lowercase the first character of text that may include links that have been passed through `full_link()` in [[Module:links]] or a similar function. ]==] function export.lcfirst(str) return uclcfirst(str, ulower) end --[==[Capitalizes each word of the input string. WARNING: May be broken in the presence of multiword links.]==] function export.capitalize(str) -- Capitalize multi-word that is separated by spaces -- by uppercasing the first letter of each part. return (ugsub(str, "%w+", ucfirst)) end local function do_title_case(first, remainder) first = uupper(first) return remainder == "" and first or (first .. ulower(remainder)) end --[==[ Capitalizes each word of the input string, with any further letters in each word being converted to lowercase. ]==] function export.title_case(str) return str == "" and "" or ugsub(str, "(%w)(%w*)", do_title_case) end title_case = export.title_case --[==[ Converts the input string to {{w|Camel case|CamelCase}}. Any non-word characters are treated as breaks between words. If `lower_first` is set, then the first character of the string will be lowercase (e.g. camelCase). ]==] function export.camel_case(str, lower_first) str = ugsub(str, "%W*(%w*)", title_case) return lower_first and do_uclcfirst(str, ulower) or str end end do local function do_snake_case(nonword, word) return nonword == "" and word or "_" .. word end --[==[ Converts the input string to {{w|Snake case|snake_case}}. Any non-word characters are treated as breaks between words. ]==] function export.snake_case(str) return (ugsub(str, "(%W*)(%w*)", do_snake_case)) end end return export ndjfpg4lokleft04xzto19wigx7fczh Mòideal:string utilities/data 828 16640 86124 2026-07-16T23:33:50Z Altronic 4137 Copy from English Wiktionary 86124 Scribunto text/plain local list_to_set = require("Module:table").listToSet local data = {} data.byte_escapes = { ["\a"] = "\\a", ["\b"] = "\\b", ["\t"] = "\\t", ["\n"] = "\\n", ["\v"] = "\\v", ["\f"] = "\\f", ["\r"] = "\\r" } data.nowiki_absolute = list_to_set{"\"", "&", "'", ";", "<", "=", ">", "[", "]", "{", "|", "}"} data.nowiki_after_newline = list_to_set{"\t", "\n", "\r", " ", "#", "*", ":"} data.nowiki_after_magic_link = list_to_set{"\t", "\n", "\f", "\r", " "} data.nowiki_uri_schemes = list_to_set{"bitcoin", "geo", "magnet", "mailto", "matrix", "news", "sip", "sips", "sms", "tel", "urn", "xmpp"} return data b91d725iejfv911ewoym32in61k1ek1 Mòideal:memoize 828 16641 86126 2026-07-16T23:52:25Z Altronic 4137 Copy from English Wiktionary 86126 Scribunto text/plain local math_module = "Module:math" local table_pack_module = "Module:table/pack" local require = require local select = select local unpack = unpack or table.unpack -- Lua 5.2 compatibility -- table.pack: in Lua 5.2+, this is a function that wraps the parameters given -- into a table with the additional key `n` that contains the total number of -- parameters given. This is not available on Lua 5.1, so [[Module:table/pack]] -- provides the same functionality. local function pack(...) pack = require(table_pack_module) return pack(...) end local function sign(...) sign = require(math_module).sign return sign(...) end ----- M E M O I Z A T I O N----- -- Memoizes a function or callable table. -- Supports any number of arguments and return values. -- If the optional parameter `simple` is set, then the memoizer will use a faster implementation, but this is only compatible with one argument and one return value. If `simple` is set, additional arguments will be accepted, but this should only be done if those arguments will always be the same. -- Sentinels. local _nil, neg_0, pos_nan, neg_nan = {}, {}, {}, {} -- Certain values can't be used as table keys, so they require sentinels as well: e.g. f("foo", nil, "bar") would be memoized at memo["foo"][_nil]["bar"][memo]. These values are: -- nil. -- -0, which is equivalent to 0 in most situations, but becomes "-0" on conversion to string; it also behaves differently in some operations (e.g. 1/a evaluates to inf if a is 0, but -inf if a is -0). -- NaN and -NaN, which are the only values for which n == n is false; they only seem to differ on conversion to string ("nan" and "-nan"). local function get_key(x) if x == x then return x == nil and _nil or x == 0 and 1 / x < 0 and neg_0 or x end return sign(x) == 1 and pos_nan or neg_nan end -- Return values are memoized as tables of return values, which are looked up using each input argument as a key, followed by `memo`. e.g. if the input arguments were (1, 2, 3), the memo would be located at t[1][2][3][memo]. `memo` is always used as the final lookup key so that (for example) the memo for f(1, 2, 3), f[1][2][3][memo], doesn't interfere with the memo for f(1, 2), f[1][2][memo]. local function get_memo(memo, n, nargs, key, ...) key = get_key(key) local next_memo = memo[key] if next_memo == nil then next_memo = {} memo[key] = next_memo end memo = next_memo return n == nargs and memo or get_memo(memo, n + 1, nargs, ...) end -- Used to catch the function output values instead of using a table directly, -- since pack() returns a table with the key `n`, giving the number of return -- values, even if they are nil. This ensures that any nil return values after -- the last non-nil value will always be present (e.g. pack() gives {n = 0}, -- pack(nil) gives {n = 1}, pack(nil, "foo", nil) gives {[2] = "foo", n = 3} -- etc.). The distinction between nil and nothing affects some native functions -- (e.g. tostring() throws an error, but tostring(nil) returns "nil"), so it -- needs to be reconstructable from the memo. local function memoize_then_return(memo, _memo, ...) _memo[memo] = pack(...) return ... end return function(func, simple) local memo = {} if simple then return function(...) local key = get_key((...)) local output = memo[key] if output == nil then output = func(...) memo[key] = output == nil and _nil or output return output elseif output == _nil then return nil end return output end end return function(...) local nargs = select("#", ...) -- Since all possible inputs need to be memoized (including true, false -- and nil), the memo table itself is used as a sentinel to ensure that -- the table of arguments will always have a unique key. local _memo = nargs == 0 and memo or get_memo(memo, 1, nargs, ...) local output = _memo[memo] -- If get_memo() returned nil, call `func` with the arguments and catch -- the output with memoize_then_return(); this packs the return values -- into a table to memoize them, then returns them. Since the return -- values are available to it as `...`, this avoids the need to call -- unpack() on the memoized table on the first call, as they can be -- returned directly. if output == nil then return memoize_then_return(memo, _memo, func(...)) end -- Unpack from 1 to the original number of return values (memoized at -- key `n`); unpack() returns nil for any values not in output. return unpack(output, 1, output.n) end end 8gmyvbgmtmwh2pu7neciscs7liw1v30 Mòideal:fun 828 16642 86127 2026-07-16T23:57:09Z Altronic 4137 Copy from English Wiktionary 86127 Scribunto text/plain local export = {} local debug_track_module = "Module:debug/track" local table_get_unprotected_metatable = "Module:table/getUnprotectedMetatable" local chain -- defined below local chain_iter -- defined below local format = string.format local gmatch = string.gmatch local ipairs = ipairs local is_callable -- defined below local pairs = pairs local pcall = pcall local rawget = rawget local require = require local select = select local tostring = tostring local type = type local unpack = unpack or table.unpack -- Lua 5.2 compatibility local unroll -- defined below local xpcall = xpcall local function debug_track(...) debug_track = require(debug_track_module) return debug_track(...) end local function get_unprotected_metatable(...) get_unprotected_metatable = require(table_get_unprotected_metatable) return get_unprotected_metatable(...) end local function _iterString(iter, i) i = i + 1 local char = iter() if char ~= nil then return i, char end end -- Iterate over UTF-8-encoded codepoints in string. local function iterString(str) return _iterString, gmatch(str, ".[\128-\191]*"), 0 end --[==[ Return {true} if the input is a function or functor (an object which can be called like a function, because it has a {__call} metamethod). Note: if the input is an object with a {__call} metamethod, but this function is not able to find it because the object's metatable is protected with {__metatable}, then it will return {false} by default, or {nil} if the {allow_maybe} flag is set.]==] function export.is_callable(f, allow_maybe) if type(f) == "function" then return true end -- An object is a functor if it has a `__call` metamethod. The only way to truly confirm this is by trying to call it, but that could be expensive or have side effects, so look for a `__call` metamethod instead. If the metatable is protected with `__metatable`, this may not be possible. local mt = get_unprotected_metatable(f) if mt == nil then return false -- `get_unprotected_metatable` returns false if the metatable is protected. elseif mt == false then debug_track("fun/is_callable/protected metatable") if allow_maybe then return nil end return false end -- `__call` metamethods have to be functions, so don't recurse to check it. local __call = rawget(mt, "__call") return __call and type(__call) == "function" or false end is_callable = export.is_callable --[==[ A version of {xpcall} which takes any arguments to be given to {f} as additional arguments after the error handler. This fixes a deficiency in the standard version of {xpcall}, which is not able to handle arguments to be given to {f}, and brings it in line with {pcall}.]==] function export.xpcall(f, err_handler, ...) -- If there are no arguments, just call xpcall() with `f`. if select("#", ...) == 0 then return xpcall(f, err_handler) end -- Any arguments have to be smuggled in via a table, as ... can't be an -- upvalue, and it's not possible to use pcall() to get aroud this, because -- xpcall() calls the error handler before the stack unwinds. local args = {...} return xpcall(function() return f(unpack(args)) end, err_handler) end do local function catch_values(f, success, ...) if success then return success, ... -- Error message will only take this exact form if `f` is not callable, -- because it will contain a traceback if it was thrown further up the -- stack. elseif (...) == format("attempt to call a %s value", type(f)) then return false end return error(...) end --[==[ A special form of {pcall()}, which returns {true} plus the result value(s) if {f} is callable, or {false} if it isn't. Errors that occur within the called function are not protected.]==] function export.try_call(f, ...) local callable = is_callable(f, true) if callable then return true, f(...) elseif callable == false then return false end -- If `callable` is nil, there's a protected metatable, so there's no way to check without doing a protected call. return catch_values(f, pcall(f, ...)) end end --[==[ Takes two or more functions as arguments, and returns a new function which calls each of the input functions in turn. Any arguments given to the returned function are given to the first function, and all other functions receive the output value(s) from the previous function.]==] function export.chain(func1, func2, ...) local function chained_func(...) return func2(func1(...)) end if select("#", ...) == 0 then return chained_func end return chain(chained_func, ...) end chain = export.chain --[==[ Takes the usual for-loop parameters (an iterator, plus an optional state and initial index), and unrolls the iterator by returning every (first) value returned by the iterator. For instance, {unroll(pairs(t))} will return every key in {t}, and {unroll(string.gmatch(s, "%w+"))} will return every word in {s}.]==] function export.unroll(iter, state, k) k = iter(state, k) if k ~= nil then return k, unroll(iter, state, k) end end unroll = export.unroll --[==[ Takes a generator function (i.e. a function that returns an iterator, such as {ipairs}) and one or more additional functions, and returns a new generator function. Any arguments given to the new generator (e.g. an input table) are given to the original generator, and the additional functions are called on each iteration. The first additional function takes the output from the original iterator (i.e. the function returned by the original generator), and any further functions receive the output value(s) from the previous function. This can be used to modify the values returned from an iterator.]==] function export.chainIter(gen, new_iter, ...) if select("#", ...) > 0 then new_iter = chain(new_iter, ...) end return function(...) local orig_iter, state, k = gen(...) -- k has to be the first value returned by orig_iter on the last iteration, not whatever new_iter returned. local function catch_values(...) k = ... if k ~= nil then return new_iter(...) end end return function() return catch_values(orig_iter(state, k)) end, state, k end end chain_iter = export.chainIter do local function catch_values(start, iter, state, k, ...) if start == k or k == nil then return k, ... end return catch_values(start, iter, state, iter(state, k)) end function export.iterateFrom(start, iter, state, k) local first = true return function(state, k) if first then first = false return catch_values(start, iter, state, iter(state, k)) end return iter(state, k) end, state, k end end -- map(function(number) return number ^ 2 end, -- { 1, 2, 3 }) --> { 1, 4, 9 } -- map(function (char) return string.char(string.byte(char) - 0x20) end, -- "abc") --> { "A", "B", "C" } function export.map(func, iterable, isArray) local array = {} for k, v in (type(iterable) == "string" and iterString or (isArray or iterable[1] ~= nil) and ipairs or pairs)(iterable) do array[k] = func(v, k, iterable) end return array end function export.mapIter(func, iter, state, init) -- init could be anything local array, i = {}, 0 for x, y in iter, state, init do i = i + 1 array[i] = func(y, x, state) end return array end do local function iter_tuples(tuples) local i = tuples.i if i > 1 then i = i - 1 tuples.i = i return unpack(tuples[i]) end end -- Takes an iterator function, and returns a new iterator that iterates in reverse, given the same arguments. -- Note: changes to the state during iteration are not taken into account, since all the return values are calculated in advance. function export.reverseIter(func) return function(...) -- Store all returned values as a list of tuples, then iterate in reverse over that list. local tuples, i, iter, state, val1 = {}, 0, func(...) while true do i = i + 1 local vals = {iter(state, val1)} -- Terminates if the first return value is nil, even if other values are non-nil. val1 = vals[1] if val1 == nil then tuples.i = i return iter_tuples, tuples end tuples[i] = vals end end end end function export.forEach(func, iterable, isArray) for k, v in (type(iterable) == "string" and iterString or (isArray or iterable[1] ~= nil) and ipairs or pairs)(iterable) do func(v, k, iterable) end return nil end ------------------------------------------------- -- From http://lua-users.org/wiki/CurriedLua -- reverse(...) : take some tuple and return a tuple of elements in reverse order -- -- e.g. "reverse(1,2,3)" returns 3,2,1 local function reverse(...) -- reverse args by building a function to do it, similar to the unpack() example local function reverseHelper(acc, v, ...) if select("#", ...) == 0 then return v, acc() else return reverseHelper(function() return v, acc() end, ...) end end -- initial acc is the end of the list return reverseHelper(function() return end, ...) end function export.curry(func, numArgs) -- currying 2-argument functions seems to be the most popular application numArgs = numArgs or 2 -- no sense currying for 1 arg or less if numArgs <= 1 then return func end -- helper takes an argTrace function, and number of arguments remaining to be applied local function curryHelper(argTrace, n) if n == 0 then -- kick off argTrace, reverse argument list, and call the original function return func(reverse(argTrace())) else -- "push" argument (by building a wrapper function) and decrement n return function(onearg) return curryHelper(function() return onearg, argTrace() end, n - 1) end end end -- push the terminal case of argTrace into the function first return curryHelper(function() return end, numArgs) end ------------------------------------------------- -- some(function(val) return val % 2 == 0 end, -- { 2, 3, 5, 7, 11 }) --> true function export.some(func, t, isArray) for k, v in ((isArray or t[1] ~= nil) and ipairs or pairs)(t) do if func(v, k, t) then return true end end return false end -- all(function(val) return val % 2 == 0 end, -- { 2, 4, 8, 10, 12 }) --> true function export.all(func, t, isArray) for k, v in ((isArray or t[1] ~= nil) and ipairs or pairs)(t) do if not func(v, k, t) then return false end end return true end function export.filter(func, t, isArray) local new_t = {} if isArray or t[1] ~= nil then -- array local new_i = 0 for i, v in ipairs(t) do if func(v, i, t) then new_i = new_i + 1 new_t[new_i] = v end end else for k, v in pairs(t) do if func(v, k, t) then new_t[k] = v -- or create array? end end end return new_t end function export.fold(func, t, accum) for i, v in ipairs(t) do accum = func(accum, v, i, t) end return accum end ------------------------------- -- Fancy stuff local function capture(...) local vals = {n = select("#", ...), ...} return function() return unpack(vals, 1, vals.n) end end -- Log input and output of function. -- Receives a function and returns a modified form of that function. function export.logReturnValues(func, prefix) return function(...) local inputValues = capture(...) local returnValues = capture(func(...)) if prefix then mw.log(prefix, inputValues()) mw.log(returnValues()) else mw.log(inputValues()) mw.log(returnValues()) end return returnValues() end end export.log = export.logReturnValues -- Convenience function to make all functions in a table log their input and output. function export.logAll(t) for k, v in pairs(t) do if is_callable(v) then t[k] = export.logReturnValues(v, tostring(k)) end end return t end return export mc1wbmetcrpjp1ihyth6j2ajabvak7y Mòideal:table/getUnprotectedMetatable 828 16643 86128 2026-07-16T23:59:16Z Altronic 4137 Copy from English Wiktionary 86128 Scribunto text/plain local _getmetatable = debug.getmetatable -- For testing (and just in case it gets enabled). if _getmetatable ~= nil then -- Avoid debug.getmetatable() throwing an error if 0 arguments are passed, -- for parity with the other function. return function(t) return _getmetatable(t) end end _getmetatable = getmetatable local pcall = pcall local rawget = rawget local setmetatable = setmetatable local type = type --[==[ Attempts to retrieve the input value's metatable, and returns it if found. If the value does not have a metatable, returns {nil}. If the input value does have a metatable, but that metatable is not possible to retrieve because it is protected with the `__metatable` metamethod, returns {false}. This is a useful way to ensure that functions can reliably distinguish between objects that do not have metamethods, objects with known metamethods, and objects with unknown metamethods.]==] return function(t) local mt = _getmetatable(t) -- If `mt` is nil, there's no metatable. if mt == nil then return nil -- If `mt` is not a table, the real metatable is protected and there's no -- way of retrieving it. elseif type(mt) ~= "table" then return false end -- Try setting `mt` as the metatable with `setmetatable`; if the metatable -- is protected, this will cause an error to be thrown (revealing it as -- protected), and if it isn't, then `mt` must be the real metatable anyway, -- so nothing has changed. Also make a special exception for data loaded via -- mw.loadData(), which sets each metatable at its own __metatable key as a -- way to stop the use of setmetatable() without actually hiding it. This is -- spoofable, but low-risk. return (pcall(setmetatable, t, mt) or rawget(mt, "mw_loadData") == true) and mt or false end oqd72lzzcljldqpmbzxsx5fm947exar Mòideal:load 828 16644 86129 2026-07-17T00:04:24Z Altronic 4137 Copy from English Wiktionary 86129 Scribunto text/plain local export = {} local load_data = mw.loadData local loaded = package.loaded local main_loader = package.loaders[2] local require = require local setmetatable = setmetatable local loaders, loaded_data, mt local function get_mt() mt, get_mt = {__mode = "kv"}, nil return mt end -- main_loader returns a loader function if the module exists, or nil if it doesn't. local function get_loader(modname) if loaders == nil then local loader = main_loader(modname) loaders = setmetatable({[modname] = loader or false}, mt or get_mt()) return loader end local loader = loaders[modname] if loader == nil then loader = main_loader(modname) loaders[modname] = loader or false end return loader or nil end local function get_data(modname, loader) if loaded_data == nil then local data = load_data(modname) loaded_data = setmetatable({[loader] = data}, mt or get_mt()) return data end local data = loaded_data[loader] if data == nil then data = load_data(modname) loaded_data[loader] = data end return data end --[==[ Like `require`, but returns `nil` if a module does not exist, instead of throwing an error. Outputs are cached, which is faster for all modules, but much faster for nonexistent modules, since `require` will attempt to use the full loader each time because loading failures don't get cached.]==] function export.safe_require(modname) local mod = loaded[modname] if mod == nil and get_loader(modname) then -- Call with require instead of the loader directly, as it protects against infinite loading loops. return require(modname) end return mod end --[==[ Like `mw.loadData`, but it does not generate a new table each time the same module is loaded via this function. Instead, it caches the returned data so that the same table can be returned each time, which is more efficient.]==] function export.load_data(modname) return get_data(modname, get_loader(modname)) end --[==[ Like `export.load_data` (itself a variant of `mw.loadData`), but returns `nil` if a module does not exist, instead of throwing an error. Outputs are cached, which is faster for all modules, but much faster for nonexistent modules, since `mw.loadData` will attempt to use the full loader each time because loading failures don't get cached.]==] function export.safe_load_data(modname) local loader = get_loader(modname) if loader == nil then return nil end return get_data(modname, loader) end return export ane0ji5isx9pytu6xaiacn5a70oo2t2 Mòideal:table/numKeys 828 16645 86130 2026-07-17T00:10:32Z Altronic 4137 Copy from English Wiktionary 86130 Scribunto text/plain local math_module = "Module:math" local pairs = pairs local sort = table.sort local function is_positive_integer(...) is_positive_integer = require(math_module).is_positive_integer return is_positive_integer(...) end --[==[ Given a table, return an array containing all positive integer keys, sorted either in numerical order, or using a custom `keySort` function.]==] return function(t, keySort) local nums, i = {}, 0 for k in pairs(t) do if is_positive_integer(k) then i = i + 1 nums[i] = k end end -- No need for [[Module:math/compare]], as the default is adequate for integers. sort(nums, keySort) return nums end 9c7afk02cuh6j5vb8qjjz77xn7dijyy Mòideal:yesno 828 16646 86131 2026-07-17T00:12:52Z Altronic 4137 Copy from English Wiktionary 86131 Scribunto text/plain -- Function allowing for consistent treatment of boolean-like wikitext input. -- It works similarly to the template {{yesno}}. local lower = string.lower local tonumber = tonumber local type = type local yesno return function (val, default) if val == nil then return nil elseif not yesno then yesno = { [true] = true, [false] = false, ["true"] = true, ["false"] = false, ["t"] = true, ["f"] = false, [1] = true, [0] = false, ["1"] = true, ["0"] = false, ["yes"] = true, ["no"] = false, ["y"] = true, ["n"] = false, ["on"] = true, ["off"] = false, } end local ret = yesno[val] if ret ~= nil then return ret elseif type(val) ~= "string" then return default end -- Catch inputs like "00". ret = yesno[tonumber(val)] if ret ~= nil then return ret end -- Case-insensitive. ret = yesno[lower(val)] if ret ~= nil then return ret end return default end 50fw3j6qj6cod4hrcb5leyd15zs34hg Mòideal:transliteration/data 828 16647 86132 2026-07-17T00:14:25Z Altronic 4137 Copy from English Wiktionary 86132 Scribunto text/plain local needs_translit = { ["alr"] = true, ["ab"] = true, ["abq"] = true, ["ady"] = true, ["agh"] = true, ["akv"] = true, ["am"] = true, ["ani"] = true, ["aqc"] = true, ["ar"] = true, ["as"] = true, ["av"] = true, ["ba"] = true, ["bbl"] = true, ["bdk"] = true, ["be"] = true, ["bg"] = true, ["bn"] = true, ["bo"] = true, ["bph"] = true, ["bxr"] = true, ["ce"] = true, ["cji"] = true, ["ckt"] = true, ["cv"] = true, ["dar"] = true, ["dlg"] = true, ["dng"] = true, ["dv"] = true, ["el"] = true, ["enf"] = true, ["ess"] = true, ["eve"] = true, ["evn"] = true, ["fa"] = true, ["gdo"] = true, ["got"] = true, ["gu"] = true, ["he"] = true, ["hi"] = true, ["hy"] = true, ["inh"] = true, ["itl"] = true, ["ja"] = true, ["ka"] = true, ["kap"] = true, ["kbd"] = true, ["kca"] = true, ["kjh"] = true, ["kk"] = true, ["km"] = true, ["kn"] = true, ["ko"] = true, ["krc"] = true, ["kv"] = true, ["kva"] = true, ["ky"] = true, ["lez"] = true, ["lo"] = true, ["mdf"] = true, ["mk"] = true, ["ml"] = true, ["mn"] = true, ["mr"] = true, ["my"] = true, ["myv"] = true, ["ne"] = true, ["or"] = true, ["os"] = true, ["ps"] = true, ["ru"] = true, ["rue"] = true, ["sah"] = true, ["sa"] = true, ["si"] = true, ["ta"] = true, ["te"] = true, ["tg"] = true, ["th"] = true, ["ti"] = true, ["tt"] = true, ["tyv"] = true, ["udm"] = true, ["ug"] = true, ["uk"] = true, ["ur"] = true, ["xal"] = true, ["yi"] = true, ["yrk-tun"] = true, } return { needs_translit } hify3vkrfmmepxnpibhlkfsp7sq7v9t Mòideal:string/char 828 16648 86133 2026-07-17T00:19:11Z Altronic 4137 Copy from English Wiktionary 86133 Scribunto text/plain local math_module = "Module:math" local char = string.char local error = error local format = string.format local pcall = pcall local select = select local tonumber = tonumber local type = type local function to_hex(...) to_hex = require(math_module).to_hex return to_hex(...) end local function codepoint_err(cp, i) -- Throw error: to_hex can only return integers, so only show the bad value -- if it can be converted into something that looks like a codepoint. local success, result = pcall(to_hex, cp, true) error(format( "bad argument #%d to 'string/char' (codepoint between 0x0 and 0x10FFFF expected%s)", i, success and "; got " .. result or ""), i + 3) end local function utf8_char(n, i, v, ...) local cp = tonumber(v) if cp == nil then error(format("bad argument #%d to 'char' (number expected; got %s)", i, type(v)), i + 2) elseif cp < 0 then codepoint_err(cp, i) elseif cp < 0x80 then if i == n then return cp end return cp, utf8_char(n, i + 1, ...) elseif cp < 0x800 then if i == n then return 0xC0 + cp / 0x40, 0x80 + cp % 0x40 end return 0xC0 + cp / 0x40, 0x80 + cp % 0x40, utf8_char(n, i + 1, ...) elseif cp < 0x10000 then -- Don't return "?" for surrogates, like mw.ustring.char does, as they -- have legitimate uses (e.g. in JSON). if i == n then return 0xE0 + cp / 0x1000, 0x80 + cp / 0x40 % 0x40, 0x80 + cp % 0x40 end return 0xE0 + cp / 0x1000, 0x80 + cp / 0x40 % 0x40, 0x80 + cp % 0x40, utf8_char(n, i + 1, ...) elseif cp < 0x110000 then if i == n then return 0xF0 + cp / 0x40000, 0x80 + cp / 0x1000 % 0x40, 0x80 + cp / 0x40 % 0x40, 0x80 + cp % 0x40 end return 0xF0 + cp / 0x40000, 0x80 + cp / 0x1000 % 0x40, 0x80 + cp / 0x40 % 0x40, 0x80 + cp % 0x40, utf8_char(n, i + 1, ...) end codepoint_err(cp, i) end return function(...) local n = select("#", ...) if n ~= 0 then return char(utf8_char(n, 1, ...)) end end qud4hkutkfwzx4yv3ba5z040h7pzaqb Teamplaid:head 10 16649 86134 2026-07-17T03:24:31Z Altronic 4137 Copy from English Wiktionary 86134 wikitext text/x-wiki <includeonly>{{#invoke:headword/templates|head_t}}<!-- --></includeonly><noinclude>{{head|und|nouns}}{{documentation}}</noinclude> akjc34wkcxijofwsgfubqlbnq1w4fqs Mòideal:headword/templates 828 16650 86135 2026-07-17T03:25:53Z Altronic 4137 Copy from English Wiktionary 86135 Scribunto text/plain local export = {} local debug_track_module = "Module:debug/track" local headword_module = "Module:headword" local parameters_module = "Module:parameters" local string_utilities_module = "Module:string utilities" local yesno_module = "Module:yesno" local insert = table.insert local require = require local tostring = tostring local function debug_track(...) debug_track = require(debug_track_module) return debug_track(...) end local function process_params(...) process_params = require(parameters_module).process return process_params(...) end local function track(page) debug_track("headword/templates/" .. page) return true end local function get_args(frame) local boolean = {type = "boolean"} local boolean_list_allow_holes = {type = "boolean", list = true, allow_holes = true} local list_allow_holes = {list = true, allow_holes = true} return process_params(frame:getParent().args, { [1] = {required = true, type = "language", template_default = "und"}, sc = {type = "script"}, sort = true, [2] = {required = true, template_default = "nouns"}, sccat = boolean, noposcat = boolean, nomultiwordcat = boolean, nogendercat = boolean, nopalindromecat = boolean, nolinkhead = boolean, autotrinfl = boolean, altform = boolean, -- EXPERIMENTAL: see [[Wiktionary:Beer parlour/2024/June#Decluttering the altform mess]] checkredlinks = true, cat2 = true, cat3 = true, cat4 = true, pagename = true, head = list_allow_holes, image = true, id = true, tr = list_allow_holes, ts = list_allow_holes, gloss = true, g = {list = true, type = "genders", flatten = true}, ["g\1qual"] = {list = true, allow_holes = true, replaced_by = false, instead = "use <q:...> or <l:...> inline modifier on a gender"}, [3] = list_allow_holes, ["f\1accel-form"] = list_allow_holes, ["f\1accel-translit"] = list_allow_holes, ["f\1accel-lemma"] = list_allow_holes, ["f\1accel-lemma-translit"] = list_allow_holes, ["f\1accel-gender"] = list_allow_holes, ["f\1accel-nostore"] = boolean_list_allow_holes, ["f\1request"] = list_allow_holes, ["f\1alt"] = list_allow_holes, ["f\1lang"] = {list = true, allow_holes = true, type = "language"}, ["f\1sc"] = {list = true, allow_holes = true, type = "script"}, ["f\1id"] = list_allow_holes, ["f\1tr"] = list_allow_holes, ["f\1ts"] = list_allow_holes, ["f\1t"] = list_allow_holes, ["f\1lit"] = list_allow_holes, ["f\1pos"] = list_allow_holes, ["f\1ng"] = list_allow_holes, ["f\1g"] = {list = true, allow_holes = true, type = "genders"}, ["f\1q"] = {list = true, allow_holes = true, type = "qualifier"}, ["f\1qq"] = {list = true, allow_holes = true, type = "qualifier"}, ["f\1qual"] = {list = true, allow_holes = true, replaced_by = false, instead = "use fNq= or fNl="}, ["f\1l"] = {list = true, allow_holes = true, type = "labels"}, ["f\1ll"] = {list = true, allow_holes = true, type = "labels"}, ["f\1ref"] = {list = true, allow_holes = true, type = "references"}, ["f\1autotr"] = boolean_list_allow_holes, ["f\1nolink"] = boolean_list_allow_holes, }) end function export.head_t(frame) local m_headword = require(headword_module) local args = get_args(frame) -- Get language and script information local data = {} data.lang = args[1] data.sc = args.sc data.sccat = args.sccat data.sort_key = args.sort data.heads = args.head data.image = args.image data.pagename = args.pagename data.id = args.id data.translits = args.tr data.transcriptions = args.ts data.gloss = args.gloss data.genders = args.g -- TODO should throw an error if data.heads gets overwritten if data.image then data.heads = {"[[File:" .. data.image .. "|class=skin-invert-image]]"} end -- This shouldn't really happen. for i = 1, args.head.maxindex do if not args.head[i] then track("head-with-holes") end end -- EXPERIMENTAL: see [[Wiktionary:Beer parlour/2024/June#Decluttering the altform mess]] data.altform = args.altform -- Part-of-speech category local pos_category = args[2] data.noposcat = args.noposcat -- Check for headword aliases and then pluralize if the POS term does not have an invariable plural. data.pos_category = m_headword.canonicalize_pos(pos_category) -- Additional categories. local categories = {} data.whole_page_categories = {} data.nomultiwordcat = args.nomultiwordcat data.nogendercat = args.nogendercat data.nopalindromecat = args.nopalindromecat -- FIXME: add a minimum_index spec to [[Module:parameters]] list specs, so -- that `cat` can be changed to a list parameter starting at index 2. if args.cat2 then insert(categories, data.lang:getFullName() .. " " .. args.cat2) end if args.cat3 then insert(categories, data.lang:getFullName() .. " " .. args.cat3) end if args.cat4 then insert(categories, data.lang:getFullName() .. " " .. args.cat4) end data.categories = categories if args.checkredlinks then data.checkredlinks = require(yesno_module)(args.checkredlinks, args.checkredlinks) end -- Headword linking data.nolinkhead = args.nolinkhead -- Inflected forms data.inflections = {enable_auto_translit = args.autotrinfl} local forms = args[3] local n = forms.maxindex / 2 for i = 1, n + n % 1 do local infl_part = { label = forms[i * 2 - 1], accel = args["faccel-form"][i] and { form = args["faccel-form"][i], translit = args["faccel-translit"][i], lemma = args["faccel-lemma"][i], lemma_translit = args["faccel-lemma-translit"][i], gender = args["faccel-gender"][i], nostore = args["faccel-nostore"][i], } or nil, request = args.frequest[i], enable_auto_translit = args.fautotr[i], } local form = { term = forms[i * 2], alt = args.falt[i], genders = args.fg[i], id = args.fid[i], lang = args.flang[i], nolinkinfl = args.fnolink[i], q = args.fq[i], qq = args.fqq[i], l = args.fl[i], ll = args.fll[i], refs = args.fref[i], sc = args.fsc[i], tr = args.ftr[i], ts = args.fts[i], gloss = args.ft[i], lit = args.flit[i], pos = args.fpos[i], ng = args.fng[i], } -- If no term or alt is given, then the label is shown alone. if form.term or form.alt then insert(infl_part, form) end if infl_part.label == "or" then -- Append to the previous inflection part, if one exists if #infl_part > 0 and data.inflections[1] then insert(data.inflections[#data.inflections], form) end elseif infl_part.label then -- Add a new inflection part insert(data.inflections, infl_part) end end return m_headword.full_headword(data) end function export.canonicalize_pos(frame) local iargs = process_params(frame.args, { [1] = {required = true}, }) return require(headword_module).canonicalize_pos(iargs[1]) end return export gb1j84xezj6e97i6exn60qidfzbyeuo Mòideal:headword 828 16651 86136 2026-07-17T03:27:55Z Altronic 4137 Copy from English Wiktionary 86136 Scribunto text/plain local export = {} -- Named constants for all modules used, to make it easier to swap out sandbox versions. local debug_track_module = "Module:debug/track" local en_utilities_module = "Module:en-utilities" local gender_and_number_module = "Module:gender and number" local headword_data_module = "Module:headword/data" local headword_page_module = "Module:headword/page" local links_module = "Module:links" local load_module = "Module:load" local pages_module = "Module:pages" local palindromes_module = "Module:palindromes" local pron_qualifier_module = "Module:pron qualifier" local scripts_module = "Module:scripts" local scripts_data_module = "Module:scripts/data" local script_utilities_module = "Module:script utilities" local script_utilities_data_module = "Module:script utilities/data" local string_utilities_module = "Module:string utilities" local table_module = "Module:table" local utilities_module = "Module:utilities" local concat = table.concat local dump = mw.dumpObject local insert = table.insert local ipairs = ipairs local max = math.max local new_title = mw.title.new local pairs = pairs local require = require local toNFC = mw.ustring.toNFC local toNFD = mw.ustring.toNFD local type = type local ufind = mw.ustring.find local ugmatch = mw.ustring.gmatch local ugsub = mw.ustring.gsub local umatch = mw.ustring.match --[==[ 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 debug_track(...) debug_track = require(debug_track_module) return debug_track(...) end local function encode_entities(...) encode_entities = require(string_utilities_module).encode_entities return encode_entities(...) end local function extend(...) extend = require(table_module).extend return extend(...) end local function find_best_script_without_lang(...) find_best_script_without_lang = require(scripts_module).findBestScriptWithoutLang return find_best_script_without_lang(...) end local function format_categories(...) format_categories = require(utilities_module).format_categories return format_categories(...) end local function format_genders(...) format_genders = require(gender_and_number_module).format_genders return format_genders(...) end local function format_pron_qualifiers(...) format_pron_qualifiers = require(pron_qualifier_module).format_qualifiers return format_pron_qualifiers(...) end local function full_link(...) full_link = require(links_module).full_link return full_link(...) end local function get_current_L2(...) get_current_L2 = require(pages_module).get_current_L2 return get_current_L2(...) end local function get_link_page(...) get_link_page = require(links_module).get_link_page return get_link_page(...) end local function get_script(...) get_script = require(scripts_module).getByCode return get_script(...) end local function is_palindrome(...) is_palindrome = require(palindromes_module).is_palindrome return is_palindrome(...) end local function language_link(...) language_link = require(links_module).language_link return language_link(...) end local function load_data(...) load_data = require(load_module).load_data return load_data(...) end local function pattern_escape(...) pattern_escape = require(string_utilities_module).pattern_escape return pattern_escape(...) end local function pluralize(...) pluralize = require(en_utilities_module).pluralize return pluralize(...) end local function process_page(...) process_page = require(headword_page_module).process_page return process_page(...) end local function remove_links(...) remove_links = require(links_module).remove_links return remove_links(...) end local function shallow_copy(...) shallow_copy = require(table_module).shallowCopy return shallow_copy(...) end local function tag_text(...) tag_text = require(script_utilities_module).tag_text return tag_text(...) end local function tag_transcription(...) tag_transcription = require(script_utilities_module).tag_transcription return tag_transcription(...) end local function tag_translit(...) tag_translit = require(script_utilities_module).tag_translit return tag_translit(...) end local function trim(...) trim = require(string_utilities_module).trim return trim(...) end local function ulen(...) ulen = require(string_utilities_module).len return ulen(...) 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 m_data local function get_data() m_data = load_data(headword_data_module) return m_data end local script_data local function get_script_data() script_data = load_data(scripts_data_module) return script_data end local script_utilities_data local function get_script_utilities_data() script_utilities_data = load_data(script_utilities_data_module) return script_utilities_data end -- If set to true, categories always appear, even in non-mainspace pages local test_force_categories = false -- Add a tracking category to track entries with certain (unusually undesirable) properties. `track_id` is an identifier -- for the particular property being tracked and goes into the tracking page. Specifically, this adds a link in the -- page text to [[Wiktionary:Tracking/headword/TRACK_ID]], meaning you can find all entries with the `track_id` property -- by visiting [[Special:WhatLinksHere/Wiktionary:Tracking/headword/TRACK_ID]]. -- -- If `lang` (a language object) is given, an additional tracking page [[Wiktionary:Tracking/headword/TRACK_ID/CODE]] is -- linked to where CODE is the language code of `lang`, and you can find all entries in the combination of `track_id` -- and `lang` by visiting [[Special:WhatLinksHere/Wiktionary:Tracking/headword/TRACK_ID/CODE]]. This makes it possible to -- isolate only the entries with a specific tracking property that are in a given language. Note that if `lang` -- references at etymology-only language, both that language's code and its full parent's code are tracked. local function track(track_id, lang) local tracking_page = "headword/" .. track_id if lang and lang:hasType("etymology-only") then debug_track{tracking_page, tracking_page .. "/" .. lang:getCode(), tracking_page .. "/" .. lang:getFullCode()} elseif lang then debug_track{tracking_page, tracking_page .. "/" .. lang:getCode()} else debug_track(tracking_page) end return true end local function text_in_script(text, script_code) local sc = get_script(script_code) if not sc then error("Internal error: Bad script code " .. script_code) end local characters = sc.characters local out if characters then text = ugsub(text, "%W", "") out = ufind(text, "[" .. characters .. "]") end if out then return true else return false end end local spacingPunctuation = "[%s%p]+" --[[ List of punctuation or spacing characters that are found inside of words. Used to exclude characters from the regex above. ]] local wordPunc = "-#%%&@־׳״'.·*’་•:᠊" local notWordPunc = "[^" .. wordPunc .. "]+" -- Format a term (either a head term or an inflection term) along with any left or right qualifiers, labels, references -- or customized separator: `part` is the object specifying the term (and `lang` the language of the term), which should -- optionally contain: -- * left qualifiers in `q`, an array of strings; -- * right qualifiers in `qq`, an array of strings; -- * left labels in `l`, an array of strings; -- * right labels in `ll`, an array of strings; -- * references in `refs`, an array either of strings (formatted reference text) or objects containing fields `text` -- (formatted reference text) and optionally `name` and/or `group`; -- * a separator in `separator`, defaulting to " <i>or</i> " if this is not the first term (j > 1), otherwise "". -- `formatted` is the formatted version of the term itself, and `j` is the index of the term. local function format_term_with_qualifiers_and_refs(lang, part, formatted, j) local function part_non_empty(field) local list = part[field] if not list then return nil end if type(list) ~= "table" then error(("Internal error: Wrong type for `part.%s`=%s, should be \"table\""):format(field, dump(list))) end return list[1] end if part_non_empty("q") or part_non_empty("qq") or part_non_empty("l") or part_non_empty("ll") or part_non_empty("refs") then formatted = format_pron_qualifiers { lang = lang, text = formatted, q = part.q, qq = part.qq, l = part.l, ll = part.ll, refs = part.refs, } end local separator = part.separator or j > 1 and " <i>or</i> " -- use "" to request no separator if separator then formatted = separator .. formatted end return formatted end --[==[Return true if the given head is multiword according to the algorithm used in full_headword().]==] function export.head_is_multiword(head) for possibleWordBreak in ugmatch(head, spacingPunctuation) do if umatch(possibleWordBreak, notWordPunc) then return true end end return false end do local function workaround_to_exclude_chars(s) return (ugsub(s, notWordPunc, "\2%1\1")) end --[==[Add links to a multiword head.]==] function export.add_multiword_links(head, default) head = "\1" .. ugsub(head, spacingPunctuation, workaround_to_exclude_chars) .. "\2" if default then head = head :gsub("(\1[^\2]*)\\([:#][^\2]*\2)", "%1\\\\%2") :gsub("(\1[^\2]*)([:#][^\2]*\2)", "%1\\%2") end --Escape any remaining square brackets to stop them breaking links (e.g. "[citation needed]"). head = encode_entities(head, "[]", true, true) --[=[ use this when workaround is no longer needed: head = "[[" .. ugsub(head, WORDBREAKCHARS, "]]%1[[") .. "]]" Remove any empty links, which could have been created above at the beginning or end of the string. ]=] return (head :gsub("\1\2", "") :gsub("[\1\2]", {["\1"] = "[[", ["\2"] = "]]"})) end end local function non_categorizable(full_raw_pagename) return full_raw_pagename:find("^Appendix:Gestures/") or -- Unsupported titles with descriptive names. (full_raw_pagename:find("^Unsupported titles/") and not full_raw_pagename:find("`")) end local function tag_text_and_add_quals_and_refs(data, head, formatted, j) -- Add language and script wrapper. formatted = tag_text(formatted, data.lang, head.sc, "head", nil, j == 1 and data.id or nil) -- Add qualifiers, labels, references and separator. return format_term_with_qualifiers_and_refs(data.lang, head, formatted, j) end -- Format a headword with transliterations. local function format_headword(data) -- Are there non-empty transliterations? local has_translits = false local has_manual_translits = false ------ Format the headwords. ------ local head_parts = {} local unique_head_parts = {} local has_multiple_heads = not not data.heads[2] for j, head in ipairs(data.heads) do if head.tr or head.ts then has_translits = true end if head.tr and head.tr_manual or head.ts then has_manual_translits = true end local formatted -- Apply processing to the headword, for formatting links and such. if head.term:find("[[", nil, true) and head.sc:getCode() ~= "Image" then formatted = language_link{term = head.term, lang = data.lang} else formatted = data.lang:makeDisplayText(head.term, head.sc, true) end local head_part = tag_text_and_add_quals_and_refs(data, head, formatted, j) insert(head_parts, head_part) -- If multiple heads, try to determine whether all heads display the same. To do this we need to effectively -- rerun the text tagging and addition of qualifiers and references, using 1 for all indices. if has_multiple_heads then local unique_head_part if j == 1 then unique_head_part = head_part else unique_head_part = tag_text_and_add_quals_and_refs(data, head, formatted, 1) end unique_head_parts[unique_head_part] = true end end local set_size = 0 if has_multiple_heads then for _ in pairs(unique_head_parts) do set_size = set_size + 1 end end if set_size == 1 then head_parts = head_parts[1] else head_parts = concat(head_parts) end if has_manual_translits then -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/manual-tr]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/manual-tr/LANGCODE]] track("manual-tr", data.lang) end ------ Format the transliterations and transcriptions. ------ local translits_formatted if has_translits then local translit_parts = {} for _, head in ipairs(data.heads) do if head.tr or head.ts then local this_parts = {} if head.tr then insert(this_parts, tag_translit(head.tr, data.lang:getCode(), "head", nil, head.tr_manual)) if head.ts then insert(this_parts, " ") end end if head.ts then insert(this_parts, "/" .. tag_transcription(head.ts, data.lang:getCode(), "head") .. "/") end insert(translit_parts, concat(this_parts)) end end translits_formatted = " (" .. concat(translit_parts, " <i>or</i> ") .. ")" local langname = data.lang:getCanonicalName() local transliteration_page = new_title(langname .. " transliteration", "Wiktionary") local saw_translit_page = false if transliteration_page and transliteration_page:getContent() then translits_formatted = " [[Wiktionary:" .. langname .. " transliteration|•]]" .. translits_formatted saw_translit_page = true end -- If data.lang is an etymology-only language and we didn't find a translation page for it, fall back to the -- full parent. if not saw_translit_page and data.lang:hasType("etymology-only") then langname = data.lang:getFullName() transliteration_page = new_title(langname .. " transliteration", "Wiktionary") if transliteration_page and transliteration_page:getContent() then translits_formatted = " [[Wiktionary:" .. langname .. " transliteration|•]]" .. translits_formatted end end else translits_formatted = "" end ------ Paste heads and transliterations/transcriptions. ------ local lemma_gloss if data.gloss then lemma_gloss = ' <span class="ib-content qualifier-content">' .. data.gloss .. '</span>' else lemma_gloss = "" end return head_parts .. translits_formatted .. lemma_gloss end local function format_headword_genders(data) local retval = "" if data.genders and data.genders[1] then if data.gloss then retval = "," end local pos_for_cat if not data.nogendercat then local no_gender_cat = (m_data or get_data()).no_gender_cat if not (no_gender_cat[data.lang:getCode()] or no_gender_cat[data.lang:getFullCode()]) then pos_for_cat = (m_data or get_data()).pos_for_gender_number_cat[data.pos_category:gsub("^reconstructed ", "")] end end local text, cats = format_genders(data.genders, data.lang, pos_for_cat) if cats then extend(data.categories, cats) end retval = retval .. "&nbsp;" .. text end return retval end -- Forward reference local format_inflections local function format_inflection_parts(data, parts) for j, part in ipairs(parts) do if type(part) ~= "table" then part = {term = part} end local partaccel = part.accel local face = part.face or "bold" if face ~= "bold" and face ~= "plain" and face ~= "hypothetical" then error("The face `" .. face .. "` " .. ( (script_utilities_data or get_script_utilities_data()).faces[face] and "should not be used for non-headword terms on the headword line." or "is invalid." )) end -- Here the final part 'or data.nolinkinfl' allows to have 'nolinkinfl=true' -- right into the 'data' table to disable inflection links of the entire headword -- when inflected forms aren't entry-worthy, e.g.: in Vulgar Latin local nolinkinfl = part.face == "hypothetical" or (part.nolink and track("nolink") or part.nolinkinfl) or ( data.nolink and track("nolink") or data.nolinkinfl) local formatted if part.label then -- FIXME: There should be a better way of italicizing a label. As is, this isn't customizable. formatted = "<i>" .. part.label .. "</i>" else -- Convert the term into a full link. Don't show a transliteration here unless enable_auto_translit is -- requested, either at the `parts` level (i.e. per inflection) or at the `data.inflections` level (i.e. -- specified for all inflections). This is controllable in {{head}} using autotrinfl=1 for all inflections, -- or fNautotr=1 for an individual inflection (remember that a single inflection may be associated with -- multiple terms). The reason for doing this is to avoid clutter in headword lines by default in languages -- where the script is relatively straightforward to read by learners (e.g. Greek, Russian), but allow it -- to be enabled in languages with more complex scripts (e.g. Arabic). -- -- FIXME: With nested inflections, should we also respect `enable_auto_translit` at the top level of the -- nested inflections structure? local tr = part.tr or not (parts.enable_auto_translit or data.inflections.enable_auto_translit) and "-" or nil -- FIXME: Temporary errors added 2025-10-03. Remove after a month or so. if part.translit then error("Internal error: Use field `tr` not `translit` for specifying an inflection part translit") end if part.transcription then error("Internal error: Use field `ts` not `transcription` for specifying an inflection part transcription") end local postprocess_annotations if part.inflections then postprocess_annotations = function(infldata) insert(infldata.annotations, format_inflections(data, part.inflections)) end end formatted = full_link( { term = not nolinkinfl and part.term or nil, alt = part.alt or (nolinkinfl and part.term or nil), lang = part.lang or data.lang, sc = part.sc or parts.sc or nil, gloss = part.gloss, pos = part.pos, lit = part.lit, id = part.id, genders = part.genders, tr = tr, ts = part.ts, accel = partaccel or parts.accel, postprocess_annotations = postprocess_annotations, }, face ) end parts[j] = format_term_with_qualifiers_and_refs(part.lang or data.lang, part, formatted, j) end local parts_output if parts[1] then parts_output = (parts.label and " " or "") .. concat(parts) elseif parts.request then parts_output = " <small>[please provide]</small>" insert(data.categories, "Requests for inflections in " .. data.lang:getFullName() .. " entries") else parts_output = "" end local parts_label = parts.label and ("<i>" .. parts.label .. "</i>") or "" return format_term_with_qualifiers_and_refs(data.lang, parts, parts_label .. parts_output, 1) end -- Format the inflections following the headword or nested after a given inflection. Declared local above. function format_inflections(data, inflections) if inflections and inflections[1] then -- Format each inflection individually. for key, infl in ipairs(inflections) do inflections[key] = format_inflection_parts(data, infl) end return concat(inflections, ", ") else return "" end end -- Format the top-level inflections following the headword. Currently this just adds parens around the -- formatted comma-separated inflections in `data.inflections`. local function format_top_level_inflections(data) local result = format_inflections(data, data.inflections) if result ~= "" then return " (" .. result .. ")" else return result end end -- Forward reference local check_red_link_inflections -- Check a single inflection (which consists of a label and zero or more terms, each possibly with nested inflections) -- for red links. If so, insert a red-link category based on `plpos` (the plural part of speech to insert in the -- category), stop further processing, and return true. If no red links found, return false. local function check_red_link_inflection_parts(data, parts, plpos) for _, part in ipairs(parts) do if type(part) ~= "table" then part = {term = part} end local term = part.term if term and not term:find("%[%[") then local stripped_physical_term = get_link_page(term, data.lang, part.sc or parts.sc or nil) if stripped_physical_term then local title = mw.title.new(stripped_physical_term) if title and not title:getContent() then insert(data.categories, data.lang:getFullName() .. " " .. plpos .. " with red links in their headword lines") return true end end end if part.inflections then if check_red_link_inflections(data, part.inflections, plpos) then return true end end end return false end -- Check a set of inflections (each of which describes a single inflection of the term, such as feminine or plural, and -- consists of a label and zero or more terms, each possibly with nested inflections) for red links. If so, insert a -- red-link category based on `plpos` (the plural part of speech to insert in the category), stop further processing, -- and return true. If no red links found, return false. function check_red_link_inflections(data, inflections, plpos) if inflections and inflections[1] then -- Check each inflection individually. for key, infl in ipairs(inflections) do if check_red_link_inflection_parts(data, infl, plpos) then return true end end end return false end -- Check the top-level inflections in `data.inflections`, along with any nested inflections, for red links. If so, -- insert a red-link category based on `plpos` (the plural part of speech to insert in the category), stop further -- processing, and return true. If no red links found, return false. local function check_red_link_inflections_top_level(data, plpos) return check_red_link_inflections(data, data.inflections, plpos) end --[==[ Returns the plural form of `pos`, a raw part of speech input, which could be singular or plural. Irregular plural POS are taken into account (e.g. "kanji" pluralizes to "kanji"). ]==] function export.pluralize_pos(pos) -- Make the plural form of the part of speech return (m_data or get_data()).irregular_plurals[pos] or pos:sub(-1) == "s" and pos or pluralize(pos) end --[==[ Return "lemma" if the given POS is a lemma, "non-lemma form" if a non-lemma form, or nil if unknown. The POS passed in must be in its plural form ("nouns", "prefixes", etc.). If you have a POS in its singular form, call {export.pluralize_pos()} above to pluralize it in a smart fashion that knows when to add "-s" and when to add "-es", and also takes into account any irregular plurals. If `best_guess` is given and the POS is in neither the lemma nor non-lemma list, guess based on whether it ends in " forms"; otherwise, return nil. ]==] function export.pos_lemma_or_nonlemma(plpos, best_guess) local m_headword_data = m_data or get_data() local isLemma = m_headword_data.lemmas -- Is it a lemma category? if isLemma[plpos] then return "lemma" end local plpos_no_recon = plpos:gsub("^reconstructed ", "") if isLemma[plpos_no_recon] then return "lemma" end -- Is it a nonlemma category? local isNonLemma = m_headword_data.nonlemmas if isNonLemma[plpos] or isNonLemma[plpos_no_recon] then return "non-lemma form" end local plpos_no_mut = plpos:gsub("^mutated ", "") if isLemma[plpos_no_mut] or isNonLemma[plpos_no_mut] then return "non-lemma form" elseif best_guess then return plpos:find(" forms$") and "non-lemma form" or "lemma" else return nil end end --[==[ Canonicalize a part of speech as specified in 2= in {{tl|head}}. This checks for POS aliases and non-lemma form aliases ending in 'f', and then pluralizes if the POS term does not have an invariable plural. ]==] function export.canonicalize_pos(pos) -- FIXME: Temporary code to throw an error for alias 'pre' (= preposition) that will go away. if pos == "pre" then -- Don't throw error on 'pref' as it's an alias for "prefix". error("POS 'pre' for 'preposition' no longer allowed as it's too ambiguous; use 'prep'") end -- Likewise for pro = pronoun. if pos == "pro" or pos == "prof" then error("POS 'pro' for 'pronoun' no longer allowed as it's too ambiguous; use 'pron'") end local m_headword_data = m_data or get_data() if m_headword_data.pos_aliases[pos] then pos = m_headword_data.pos_aliases[pos] elseif pos:sub(-1) == "f" then pos = pos:sub(1, -2) pos = (m_headword_data.pos_aliases[pos] or pos) .. " forms" end return export.pluralize_pos(pos) end -- Find and return the maximum index in the array `data[element]` (which may have gaps in it), and initialize it to a -- zero-length array if unspecified. Check to make sure all keys are numeric (other than "maxindex", which is set by -- [[Module:parameters]] for list parameters), all values are strings, and unless `allow_blank_string` is given, -- no blank (zero-length) strings are present. local function init_and_find_maximum_index(data, element, allow_blank_string) local maxind = 0 if not data[element] then data[element] = {} end local typ = type(data[element]) if typ ~= "table" then error(("Internal error: In full_headword(), `data.%s` must be an array but is a %s"):format(element, typ)) end for k, v in pairs(data[element]) do if k ~= "maxindex" then if type(k) ~= "number" then error(("Internal error: Unrecognized non-numeric key '%s' in `data.%s`"):format(k, element)) end if k > maxind then maxind = k end if v then if type(v) ~= "string" then error(("Internal error: For key '%s' in `data.%s`, value should be a string but is a %s"):format(k, element, type(v))) end if not allow_blank_string and v == "" then error(("Internal error: For key '%s' in `data.%s`, blank string not allowed; use 'false' for the default"):format(k, element)) end end end end return maxind end --[==[ -- Add the page to various maintenance categories for the language and the -- whole page. These are placed in the headword somewhat arbitrarily, but -- mainly because headword templates are mandatory for entries (meaning that -- in theory it provides full coverage). -- -- This is provided as an external entry point so that modules which transclude -- information from other entries (such as {{tl|ja-see}}) can take advantage -- of this feature as well, because they are used in place of a conventional -- headword template.]==] do -- Handle any manual sortkeys that have been specified in raw categories -- by tracking if they are the same or different from the automatically- -- generated sortkey, so that we can track them in maintenance -- categories. local function handle_raw_sortkeys(tbl, sortkey, page, lang, lang_cats) sortkey = sortkey or lang:makeSortKey(page.pagename) -- If there are raw categories with no sortkey, then they will be -- sorted based on the default MediaWiki sortkey, so we check against -- that. if tbl == true then if page.raw_defaultsort ~= sortkey then insert(lang_cats, lang:getFullName() .. " terms with non-redundant non-automated sortkeys") end return end local redundant, different for k in pairs(tbl) do if k == sortkey then redundant = true else different = true end end if redundant then insert(lang_cats, lang:getFullName() .. " terms with redundant sortkeys") end if different then insert(lang_cats, lang:getFullName() .. " terms with non-redundant non-automated sortkeys") end return sortkey end function export.maintenance_cats(page, lang, lang_cats, page_cats) extend(page_cats, page.cats) lang = lang:getFull() -- since we are just generating categories local canonical = lang:getCanonicalName() local tbl, sortkey = page.wikitext_topic_cat[lang:getCode()] if tbl then sortkey = handle_raw_sortkeys(tbl, sortkey, page, lang, lang_cats) insert(lang_cats, canonical .. " entries with topic categories using raw markup") end tbl = page.wikitext_langname_cat[canonical] if tbl then handle_raw_sortkeys(tbl, sortkey, page, lang, lang_cats) insert(lang_cats, canonical .. " entries with language name categories using raw markup") end if get_current_L2() ~= canonical then insert(lang_cats, canonical .. " entries with incorrect language header") -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/incorrect language header]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/incorrect language header/LANGCODE]] track("incorrect language header", lang) end end end --[==[This is the primary external entry point. {{lua|full_headword(data)}} This is used by {{temp|head}} and various language-specific headword templates (e.g. {{temp|ru-adj}} for Russian adjectives, {{temp|de-noun}} for German nouns, etc.) to display an entire headword line. See [[#Further explanations for full_headword()]] ]==] function export.full_headword(data) -- Prevent data from being destructively modified. local data = shallow_copy(data) ------------ 1. Basic checks for old-style (multi-arg) calling convention. ------------ if data.getCanonicalName then error("Internal error: In full_headword(), the first argument `data` needs to be a Lua object (table) of properties, not a language object") end if not data.lang or type(data.lang) ~= "table" or not data.lang.getCode then error("Internal error: In full_headword(), the first argument `data` needs to be a Lua object (table) and `data.lang` must be a language object") end if data.id and type(data.id) ~= "string" then error("Internal error: The id in the data table should be a string.") end ------------ 2. Initialize pagename etc. ------------ local langcode = data.lang:getCode() local full_langcode = data.lang:getFullCode() local langname = data.lang:getCanonicalName() local full_langname = data.lang:getFullName() local raw_pagename = data.pagename local page local m_headword_data = m_data or get_data() if raw_pagename and raw_pagename ~= m_headword_data.pagename then -- for testing, doc pages, etc. -- data.pagename is often set on documentation and test pages through the pagename= parameter of various -- templates, to emulate running on that page. Having a large number of such test templates on a single -- page often leads to timeouts, because we fetch and parse the contents of each page in turn. However, -- we don't really need to do that and can function fine without fetching and parsing the contents of a -- given page, so turn off content fetching/parsing (and also setting the DEFAULTSORT key through a parser -- function, which is *slooooow*) in certain namespaces where test and documentation templates are likely to -- be found and where actual content does not live (User, Template, Module). local actual_namespace = m_headword_data.page.namespace local no_fetch_content = actual_namespace == "User" or actual_namespace == "Template" or actual_namespace == "Module" page = process_page(raw_pagename, no_fetch_content) else page = m_headword_data.page end local namespace = page.namespace ------------ 3. Initialize `data.heads` table; if old-style, convert to new-style. ------------ if type(data.heads) == "table" and type(data.heads[1]) == "table" then -- new-style if data.translits or data.transcriptions then error("Internal error: In full_headword(), if `data.heads` is new-style (array of head objects), `data.translits` and `data.transcriptions` cannot be given") end else -- convert old-style `heads`, `translits` and `transcriptions` to new-style local maxind = max( init_and_find_maximum_index(data, "heads"), init_and_find_maximum_index(data, "translits", true), init_and_find_maximum_index(data, "transcriptions", true) ) for i = 1, maxind do data.heads[i] = { term = data.heads[i], tr = data.translits[i], ts = data.transcriptions[i], } end end -- Make sure there's at least one head. if not data.heads[1] then data.heads[1] = {} end ------------ 4. Initialize and validate `data.categories` and `data.whole_page_categories`, and determine `pos_category` if not given, and add basic categories. ------------ -- EXPERIMENTAL: see [[Wiktionary:Beer parlour/2024/June#Decluttering the altform mess]] if data.altform then data.noposcat = true end init_and_find_maximum_index(data, "categories") init_and_find_maximum_index(data, "whole_page_categories") local pos_category_already_present = false if data.categories[1] then local escaped_langname = pattern_escape(full_langname) local matches_lang_pattern = "^" .. escaped_langname .. " " for _, cat in ipairs(data.categories) do -- Does the category begin with the language name? If not, tag it with a tracking category. if not cat:find(matches_lang_pattern) then -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/no lang category]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/no lang category/LANGCODE]] track("no lang category", data.lang) end end -- If `pos_category` not given, try to infer it from the first specified category. If this doesn't work, we -- throw an error below. if not data.pos_category and data.categories[1]:find(matches_lang_pattern) then data.pos_category = data.categories[1]:gsub(matches_lang_pattern, "") -- Optimization to avoid inserting category already present. pos_category_already_present = true end end if not data.pos_category then error("Internal error: `data.pos_category` not specified and could not be inferred from the categories given in " .. "`data.categories`. Either specify the plural part of speech in `data.pos_category` " .. "(e.g. \"proper nouns\") or ensure that the first category in `data.categories` is formed from the " .. "language's canonical name plus the plural part of speech (e.g. \"Norwegian Bokmål proper nouns\")." ) end -- Insert a category at the beginning for the part of speech unless it's already present or `data.noposcat` given. if not pos_category_already_present and not data.noposcat then local pos_category = full_langname .. " " .. data.pos_category -- FIXME: [[User:Theknightwho]] Why is this special case here? Please add an explanatory comment. if pos_category ~= "Translingual Han characters" then insert(data.categories, 1, pos_category) end end -- Try to determine whether the part of speech refers to a lemma or a non-lemma form; if we can figure this out, -- add an appropriate category. local postype = export.pos_lemma_or_nonlemma(data.pos_category) if not postype then -- We don't know what this category is, so tag it with a tracking category. -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/LANGCODE]] track("unrecognized pos", data.lang) -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/POS]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/POS/LANGCODE]] track("unrecognized pos/pos/" .. data.pos_category, data.lang) elseif not data.noposcat then insert(data.categories, 1, full_langname .. " " .. postype .. "s") end -- EXPERIMENTAL: see [[Wiktionary:Beer parlour/2024/June#Decluttering the altform mess]] if data.altform then insert(data.categories, 1, full_langname .. " alternative forms") end ------------ 5. Create a default headword, and add links to multiword page names. ------------ -- Determine if this is an "anti-asterisk" term, i.e. an attested term in a language that must normally be -- reconstructed. local is_anti_asterisk = data.heads[1].term and data.heads[1].term:find("^!!") local lang_reconstructed = data.lang:hasType("reconstructed") if is_anti_asterisk then if not lang_reconstructed then error("Anti-asterisk feature (head= beginning with !!) can only be used with reconstructed languages") end lang_reconstructed = false end -- Determine if term is reconstructed local is_reconstructed = namespace == "Reconstruction" or lang_reconstructed -- Create a default headword based on the pagename, which is determined in -- advance by the data module so that it only needs to be done once. local default_head = page.pagename -- Add links to multi-word page names when appropriate if not (is_reconstructed or data.nolinkhead) then local no_links = m_headword_data.no_multiword_links if not (no_links[langcode] or no_links[full_langcode]) and export.head_is_multiword(default_head) then default_head = export.add_multiword_links(default_head, true) end end if is_reconstructed then default_head = "*" .. default_head end ------------ 6. Check the namespace against the language type. ------------ if namespace == "" then if lang_reconstructed then error("Entries in " .. langname .. " must be placed in the Reconstruction: namespace") elseif data.lang:hasType("appendix-constructed") then error("Entries in " .. langname .. " must be placed in the Appendix: namespace") end elseif namespace == "Citations" or namespace == "Thesaurus" then error("Headword templates should not be used in the " .. namespace .. ": namespace.") end ------------ 7. Fill in missing values in `data.heads`. ------------ -- True if any script among the headword scripts has spaces in it. local any_script_has_spaces = false -- True if any term has a redundant head= param. local has_redundant_head_param = false for _, head in ipairs(data.heads) do ------ 7a. If missing head, replace with default head. if not head.term then head.term = default_head elseif head.term == default_head then has_redundant_head_param = true elseif is_anti_asterisk and head.term == "!!" then -- If explicit head=!! is given, it's an anti-asterisk term and we fill in the default head. head.term = "!!" .. default_head elseif head.term:find("^[!?]$") then -- If explicit head= just consists of ! or ?, add it to the end of the default head. head.term = default_head .. head.term end head.term_no_initial_bang_bang = is_anti_asterisk and head.term:sub(3) or head.term if is_reconstructed then local head_term = head.term if head_term:find("%[%[") then head_term = remove_links(head_term) end if head_term:sub(1, 1) ~= "*" then error("The headword '" .. head_term .. "' must begin with '*' to indicate that it is reconstructed.") end end ------ 7b. Try to detect the script(s) if not provided. If a per-head script is provided, that takes precedence, ------ otherwise fall back to the overall script if given. If neither given, autodetect the script. local auto_sc = data.lang:findBestScript(head.term) if ( auto_sc:getCode() == "None" and find_best_script_without_lang(head.term):getCode() ~= "None" ) then insert(data.categories, full_langname .. " terms in nonstandard scripts") end if not (head.sc or data.sc) then -- No script code given, so use autodetected script. head.sc = auto_sc else if not head.sc then -- Overall script code given. head.sc = data.sc end -- Track uses of sc parameter. if head.sc:getCode() == auto_sc:getCode() then track("redundant script code", data.lang) if not data.no_script_code_cat then insert(data.categories, full_langname .. " terms with redundant script codes") end else track("non-redundant manual script code", data.lang) if not data.no_script_code_cat then insert(data.categories, full_langname .. " terms with non-redundant manual script codes") end end end -- If using a discouraged character sequence, add to maintenance category. if head.sc:hasNormalizationFixes() == true then local composed_head = toNFC(head.term) if head.sc:fixDiscouragedSequences(composed_head) ~= composed_head then insert(data.whole_page_categories, "Pages using discouraged character sequences") end end any_script_has_spaces = any_script_has_spaces or head.sc:hasSpaces() ------ 7c. Create automatic transliterations for any non-Latin headwords without manual translit given ------ (provided automatic translit is available, e.g. not in Persian or Hebrew). -- Make transliterations head.tr_manual = nil -- Try to generate a transliteration if necessary if head.tr == "-" then head.tr = nil else local notranslit = m_headword_data.notranslit if not (notranslit[langcode] or notranslit[full_langcode]) and head.sc:isTransliterated() then head.tr_manual = not not head.tr local text = head.term_no_initial_bang_bang if not data.lang:link_tr(head.sc) then text = remove_links(text) end local automated_tr = data.lang:transliterate(text, head.sc) if automated_tr then local manual_tr = head.tr if manual_tr then if remove_links(manual_tr) == remove_links(automated_tr) then insert(data.categories, full_langname .. " terms with redundant transliterations") else insert(data.categories, full_langname .. " terms with non-redundant manual transliterations") end end if not manual_tr then head.tr = automated_tr end end -- There is still no transliteration? -- Add the entry to a cleanup category. if not head.tr then head.tr = "<small>transliteration needed</small>" -- FIXME: No current support for 'Request for transliteration of Classical Persian terms' or similar. -- Consider adding this support in [[Module:category tree/poscatboiler/data/entry maintenance]]. insert(data.categories, "Requests for transliteration of " .. full_langname .. " terms") else -- Otherwise, trim it. head.tr = trim(head.tr) end end end -- Link to the transliteration entry for languages that require this. if head.tr and data.lang:link_tr(head.sc) then head.tr = full_link{ term = head.tr, lang = data.lang, sc = get_script("Latn"), tr = "-" } end end ------------ 8. Maybe tag the title with the appropriate script code, using the `display_title` mechanism. ------------ -- Assumes that the scripts in "toBeTagged" will never occur in the Reconstruction namespace. -- (FIXME: Don't make assumptions like this, and if you need to do so, throw an error if the assumption is violated.) -- Avoid tagging ASCII as Hani even when it is tagged as Hani in the headword, as in [[check]]. The check for ASCII -- might need to be expanded to a check for any Latin characters and whitespace or punctuation. local display_title -- Where there are multiple headwords, use the script for the first. This assumes the first headword is similar to -- the pagename, and that headwords that are in different scripts from the pagename aren't first. This seems to be -- about the best we can do (alternatively we could potentially do script detection on the pagename). local dt_script = data.heads[1].sc local dt_script_code = dt_script:getCode() local page_non_ascii = namespace == "" and not page.pagename:find("^[%z\1-\127]+$") local unsupported_pagename, unsupported = page.full_raw_pagename:gsub("^Unsupported titles/", "") if unsupported == 1 and page.unsupported_titles[unsupported_pagename] then display_title = 'Unsupported titles/<span class="' .. dt_script_code .. '">' .. page.unsupported_titles[unsupported_pagename] .. '</span>' elseif page_non_ascii and m_headword_data.toBeTagged[dt_script_code] or (dt_script_code == "Jpan" and (text_in_script(page.pagename, "Hira") or text_in_script(page.pagename, "Kana"))) or (dt_script_code == "Kore" and text_in_script(page.pagename, "Hang")) then display_title = '<span class="' .. dt_script_code .. '">' .. page.full_raw_pagename .. '</span>' -- Keep Han entries region-neutral in the display title. elseif page_non_ascii and (dt_script_code == "Hant" or dt_script_code == "Hans") then display_title = '<span class="Hani">' .. page.full_raw_pagename .. '</span>' elseif namespace == "Reconstruction" then local matched display_title, matched = ugsub( page.full_raw_pagename, "^(Reconstruction:[^/]+/)(.+)$", function(before, term) return before .. tag_text(term, data.lang, dt_script) end ) if matched == 0 then display_title = nil end end -- FIXME: Generalize this. -- If the current language uses ur-Arab (for Urdu, etc.), ku-Arab (Central Kurdish) or pa-Arab -- (Shahmukhi, for Punjabi) and there's more than one language on the page, don't set the display title -- because these three scripts display in Nastaliq and we don't want this for terms that also exist in other -- languages that don't display in Nastaliq (e.g. Arabic or Persian) to display in Nastaliq. Because the word -- "Urdu" occurs near the end of the alphabet, Urdu fonts tend to override the fonts of other languages. -- FIXME: This is checking for more than one language on the page but instead needs to check if there are any -- languages using scripts other than the ones just mentioned. if (dt_script_code == "ur-Arab" or dt_script_code == "ku-Arab" or dt_script_code == "pa-Arab") and page.L2_list.n > 1 then display_title = nil end if display_title then mw.getCurrentFrame():callParserFunction( "DISPLAYTITLE", display_title ) end ------------ 9. Insert additional categories. ------------ if data.force_cat_output then -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/force cat output]] track("force cat output") end if has_redundant_head_param then if not data.no_redundant_head_cat then -- This is not the right way to go about this; too many exceptions and problems due to language-specific headword -- handling customization. If we want this, it should be opt-in by a given language passing in the default headword. -- insert(data.categories, full_langname .. " terms with redundant head parameter") end end -- If the first head is multiword (after removing links), maybe insert into "LANG multiword terms". if not data.nomultiwordcat and any_script_has_spaces and postype == "lemma" then local no_multiword_cat = m_headword_data.no_multiword_cat if not (no_multiword_cat[langcode] or no_multiword_cat[full_langcode]) then -- Check for spaces or hyphens, but exclude prefixes and suffixes. -- Use the pagename, not the head= value, because the latter may have extra -- junk in it, e.g. superscripted text that throws off the algorithm. local no_hyphen = m_headword_data.hyphen_not_multiword_sep -- Exclude hyphens if the data module states that they should for this language. local checkpattern = (no_hyphen[langcode] or no_hyphen[full_langcode]) and ".[%s፡]." or ".[%s%-፡]." local is_multiword = umatch(page.pagename, checkpattern) if is_multiword and not non_categorizable(page.full_raw_pagename) then insert(data.categories, full_langname .. " multiword terms") elseif not is_multiword then local long_word_threshold = m_headword_data.long_word_thresholds[langcode] or m_headword_data.long_word_thresholds[full_langcode] if long_word_threshold and ulen(page.pagename) >= long_word_threshold then insert(data.categories, "Long " .. full_langname .. " words") end end end end local default_sccat = m_headword_data.default_sccat if data.sccat or data.sccat == nil and (default_sccat[langcode] or default_sccat[full_langcode]) then for _, head in ipairs(data.heads) do insert(data.categories, full_langname .. " " .. data.pos_category .. " in " .. head.sc:getDisplayForm()) end end -- Reconstructed terms often use weird combinations of scripts and realistically aren't spelled so much as notated. if namespace ~= "Reconstruction" then -- Map from languages to a string containing the characters to ignore when considering whether a term has -- multiple written scripts in it. Typically these are Greek or Cyrillic letters used for their phonetic -- values. local characters_to_ignore = { ["aaq"] = "αάὰ", -- Penobscot (Algonquian) ["acy"] = "δθ", -- Cypriot Arabic ["aez"] = "β", -- Aeka (Trans-New Guinea) ["anc"] = "γ", -- Ngas (Chadic/Afroasiatic) ["aou"] = "χ", -- A'ou (Kra-Dai) ["art-blk"] = "ч", -- Bolak (conlang) ["awg"] = "β", -- Anguthimri (Pama-Nyungan) ["az"] = "ь", -- Azerbaijani (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["ba"] = "ь", -- Bashkir (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["bhp"] = "β", -- Bima (Austronesian) ["bjz"] = "β", -- Baruga (Trans-New Guinea) ["byk"] = "θ", -- Biao (Kra-Dai) ["cdy"] = "θ", -- Chadong (Kra-Dai) ["chp"] = "θ", -- Chipewyan (Athabaskan) ["cjh"] = "χ", -- Upper Chehalis (Salishan) ["clm"] = "χ", -- Klallam (Salishan) ["col"] = "χ", -- Colombia-Wenatchi (Salishan) ["coo"] = "χθ", -- Comox (Salishan) ["crx"] = "θ", -- Carrier (Athabaskan) ["ets"] = "θ", -- Yekhee (Edoid/Niger-Congo) ["ett"] = "χ", -- Etruscan (isolate; in romanizations) ["fla"] = "χ", -- Montana Salish (Salishan) ["grt"] = "་", -- Garo (South Asian Sino-Tibetan) ["gmw-gts"] = "χ", -- Gottscheerish (Bavarian variant spoken in Slovenia) ["hur"] = "χθ", -- Halkomelem (Salishan) ["itc-psa"] = "f", -- Pre-Samnite (Italic; normally written in Greek) ["izh"] = "ь", -- Ingrian (Finnic) ["kic"] = "θ", -- Kickapoo (Algonquian) ["kk"] = "ь", -- Kazakh (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["ky"] = "ь", -- Kyrgyz (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["lil"] = "χ", -- Lillooet (Salishan) ["lsi"] = "ꓹ", -- Lashi (Lolo-Burmese/Sino-Tibetan; represents a glottal stop) ["mhz"] = "β", -- Mor (Austronesian) ["mqn"] = "β", -- Moronene (Austronesian) ["neg"]= "ӡā", -- Negidal (Tungusic; normally in Cyrillic) ["oka"] = "χ", -- Okanagan (Salishan) ["ole"] = "θ", -- Olekha (Sino-Tibetan) ["oui"] = "γβ", -- Old Uyghur (Turkic; FIXME: others? E.g. Greek delta (δ)?) ["pox"] = "χ", -- Polabian (West Slavic) ["rif"] = "ε", -- Tarifit (Berber) ["rom"] = "Θθ", -- Romani (Indic: International Standard; two different thetas???) ["rpn"] = "β", -- Repanbitip (Austronesian) ["sah"] = "ь", -- Yakut (Turkic; 1929 - 1939 Latin spelling) ["sit-jap"] = "χ", -- Japhug (Sino-Tibetan) ["sjw"] = "θ", -- Shawnee (Algonquian) ["squ"] = "χ", -- Squamish (Salishan) ["str"] = "χθ", -- Saanich (Salishan) ["teh"] = "χ", -- Tehuelche (Chonan; spoken in Argentina) ["tep"] = "η", -- Tepecano (Uto-Aztecan) ["thp"] = "χ", -- Thompson (Salishan) ["tk"] = "ь", -- Turkmen (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["tt"] = "ь", -- Kazakh (Turkic; Yañalif Latin spelling, c. 1928 - 1938) ["twa"] = "χ", -- Twana (Salishan) ["wbl"] = "ы", -- Wakhi (Iranian) ["xbc"] = "ϸ", -- Bactrian (Iranian; represents š; normally written in Greek) ["yha"] = "θ", -- Baha (Kra-Dai) ["za"] = "зч", -- Zhuang (Tai/Kra-Dai); 1957-1982 alphabet used two Cyrillic letters (as well as some others like -- ƃ, ƅ, ƨ, ɯ and ɵ that look like Cyrillic or Greek but are actually Latin) ["zlw-slv"] = "χђћ", -- Slovincian (West Slavic; FIXME: χ is Greek, the other two are Cyrillic, but I'm not sure -- the currect characters are being chosen in the entry names) ["zng"] = "θ", -- Mang (Mon-Khmer) ["ztp"] = "θ", -- Loxicha Zapotec (Zapotecan) } -- Determine how many real scripts are found in the pagename, where we exclude symbols and such. We exclude -- scripts whose `character_category` is false as well as Zmth (mathematical notation symbols), which has a -- category of "Mathematical notation symbols". When counting scripts, we need to elide language-specific -- variants because e.g. Beng and as-Beng have slightly different characters but we don't want to consider them -- two different scripts (e.g. [[এৰ]] has two characters which are detected respectively as Beng and as-Beng). local seen_scripts = {} local num_seen_scripts = 0 local num_loops = 0 local canon_pagename = page.pagename local ch_to_ignore = characters_to_ignore[full_langcode] if ch_to_ignore then canon_pagename = ugsub(canon_pagename, "[" .. ch_to_ignore .. "]", "") end while true do if canon_pagename == "" or num_seen_scripts >= 2 or num_loops >= 10 then break end -- Make sure we don't get into a loop checking the same script over and over again; happens with e.g. [[ᠪᡳ]] num_loops = num_loops + 1 local pagename_script = find_best_script_without_lang(canon_pagename, "None only as last resort") local script_chars = pagename_script.characters if not script_chars then -- we are stuck; this happens with None break end local script_code = pagename_script:getCode() local replaced canon_pagename, replaced = ugsub(canon_pagename, "[" .. script_chars .. "]", "") if ( replaced and script_code ~= "Zmth" and (script_data or get_script_data())[script_code] and script_data[script_code].character_category ~= false ) then script_code = script_code:gsub("^.-%-", "") if not seen_scripts[script_code] then seen_scripts[script_code] = true num_seen_scripts = num_seen_scripts + 1 end end end if num_seen_scripts > 1 then insert(data.categories, full_langname .. " terms written in multiple scripts") end end -- Categorise for unusual characters. Takes into account combining characters, so that we can categorise for characters with diacritics that aren't encoded as atomic characters (e.g. U̠). These can be in two formats: single combining characters (i.e. character + diacritic(s)) or double combining characters (i.e. character + diacritic(s) + character). Each can have any number of diacritics. local standard = data.lang:getStandardCharacters() if standard and not non_categorizable(page.full_raw_pagename) then local function char_category(char) local specials = { ["#"] = "number sign", ["("] = "parentheses", [")"] = "parentheses", ["<"] = "angle brackets", [">"] = "angle brackets", ["["] = "square brackets", ["]"] = "square brackets", ["_"] = "underscore", ["{"] = "braces", ["|"] = "vertical line", ["}"] = "braces", ["ß"] = "ẞ", ["\205\133"] = "", -- this is UTF-8 for U+0345 ( ͅ) ["\239\191\189"] = "replacement character", } char = toNFD(char) :gsub(".[\128-\191]*", function(m) local new_m = specials[m] new_m = new_m or m:uupper() return new_m end) return toNFC(char) end if full_langcode ~= "hi" and full_langcode ~= "lo" then local standard_chars_scripts = {} for _, head in ipairs(data.heads) do standard_chars_scripts[head.sc:getCode()] = true end -- Iterate over the scripts, in case there is more than one (as they can have different sets of standard characters). for code in pairs(standard_chars_scripts) do local sc_standard = data.lang:getStandardCharacters(code) if sc_standard then if page.pagename_len > 1 then local explode_standard = {} local function explode(char) explode_standard[char] = true return "" end local sc_standard = ugsub(sc_standard, page.comb_chars.combined_double, explode) sc_standard = ugsub(sc_standard,page.comb_chars.combined_single, explode) :gsub(".[\128-\191]*", explode) local num_cat_inserted for char in pairs(page.explode_pagename) do if not explode_standard[char] then if char:find("[0-9]") then if not num_cat_inserted then insert(data.categories, full_langname .. " terms spelled with numbers") num_cat_inserted = true end elseif ufind(char, page.emoji_pattern) then insert(data.categories, full_langname .. " terms spelled with emoji") else local upper = char_category(char) if not explode_standard[upper] then char = upper end insert(data.categories, full_langname .. " terms spelled with " .. char) end end end end -- If a diacritic doesn't appear in any of the standard characters, also categorise for it generally. sc_standard = toNFD(sc_standard) for diacritic in ugmatch(page.decompose_pagename, page.comb_chars.diacritics_single) do if not umatch(sc_standard, diacritic) then insert(data.categories, full_langname .. " terms spelled with ◌" .. diacritic) end end for diacritic in ugmatch(page.decompose_pagename, page.comb_chars.diacritics_double) do if not umatch(sc_standard, diacritic) then insert(data.categories, full_langname .. " terms spelled with ◌" .. diacritic .. "◌") end end end end -- Ancient Greek, Hindi and Lao handled the old way for now, as their standard chars still need to be converted to the new format (because there are a lot of them). elseif ulen(page.pagename) ~= 1 then for character in ugmatch(page.pagename, "([^" .. standard .. "])") do local upper = char_category(character) if not umatch(upper, "[" .. standard .. "]") then character = upper end insert(data.categories, full_langname .. " terms spelled with " .. character) end end end if data.heads[1].sc:isSystem("alphabet") then local pagename, i = page.pagename:ulower(), 2 while umatch(pagename, "(%a)" .. ("%1"):rep(i)) do i = i + 1 insert(data.categories, full_langname .. " terms with " .. i .. " consecutive instances of the same letter") end end -- Categorise for palindromes if not data.nopalindromecat and namespace ~= "Reconstruction" and ulen(page.pagename) > 2 -- FIXME: Use of first script here seems hacky. What is the clean way of doing this in the presence of -- multiple scripts? and is_palindrome(page.pagename, data.lang, data.heads[1].sc) then insert(data.categories, full_langname .. " palindromes") end if namespace == "" and not lang_reconstructed then for _, head in ipairs(data.heads) do if page.full_raw_pagename ~= get_link_page(remove_links(head.term), data.lang, head.sc) then -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/pagename spelling mismatch]] -- [[Special:WhatLinksHere/Wiktionary:Tracking/headword/pagename spelling mismatch/LANGCODE]] track("pagename spelling mismatch", data.lang) break end end end -- Add red link category if called for and we're not a "large" page, where such checks are disabled. if data.checkredlinks and not m_headword_data.large_pages[m_headword_data.pagename] then local plposcat = type(data.checkredlinks) == "string" and data.checkredlinks or data.pos_category check_red_link_inflections_top_level(data, plposcat) end -- Add to various maintenance categories. export.maintenance_cats(page, data.lang, data.categories, data.whole_page_categories) ------------ 10. Format and return headwords, genders, inflections and categories. ------------ -- Format and return all the gathered information. This may add more categories (e.g. gender/number categories), -- so make sure we do it before evaluating `data.categories`. local text = '<span class="headword-line">' .. format_headword(data) .. format_headword_genders(data) .. format_top_level_inflections(data) .. '</span>' -- Language-specific categories. local cats = format_categories( data.categories, data.lang, data.sort_key, page.encoded_pagename, data.force_cat_output or test_force_categories, data.heads[1].sc ) -- Language-agnostic categories. local whole_page_cats = format_categories( data.whole_page_categories, nil, "-" ) return text .. cats .. whole_page_cats end return export hiwez7r1jj6upn4uih38t4pu6mn3yjb Mòideal:headword/data 828 16652 86137 2026-07-17T03:29:16Z Altronic 4137 Copy from English Wiktionary (will need to clean up/translate these) 86137 Scribunto text/plain local headword_page_module = "Module:headword/page" local list_to_set = require("Module:table").listToSet local data = {} ------ 1. Lists which are converted into sets. ------ --[==[ var: Large pages where we disable label tracking, red link checking and similar. ]==] data.large_pages = list_to_set { -- pages that consistently hit timeouts "a", -- pages that sometimes hit timeouts "A", "baba", "de", "e", "i", "lima", "o", "u", "и", "山", "子", "月", "一", "人", } --[==[ var: Map from singular to plural, and from plural to itself, for recognized parts of speech with irregular plurals. Most of these are invariable plurals, e.g. `kanji` is its own plural; but we also have `mora` plural `morae`. ]==] data.irregular_plurals = list_to_set({ "cmavo", "cmene", "fu'ivla", "gismu", "Han tu", "hanja", "hanzi", "jyutping", "kana", "kanji", "lujvo", "phrasebook", "pinyin", "rafsi", }, function(_, item) return item end) local irregular_plurals = data.irregular_plurals -- Irregular non-zero plurals AND any regular plurals where the singular ends in "s", -- because the module assumes that inputs ending in "s" are plurals. The singular and -- plural both need to be added, as the module will generate a default plural if -- the input doesn't match a key in this table. for sg, pl in next, { mora = "morae" } do irregular_plurals[sg], irregular_plurals[pl] = pl, pl end --[==[ var: Recognized lemmas. If the part of speech in {{tl|head}} is set to one of these or its singular equivalent, the category 'LANG lemmas' will automatically be added. If the part of speech is not a singular or plural lemma or non-lemma form and is not an abbreviation that expands to a recognized lemma or non-lemma form, the page will be added to various tracking categories: * [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos]] * [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/LANG]] * [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/pos/POS]] * [[Special:WhatLinksHere/Wiktionary:Tracking/headword/unrecognized pos/pos/POS/LANG]] ]==] data.lemmas = list_to_set{ "abbreviations", "acronyms", "adjectives", "adnominals", "adpositions", "adverbs", "affixes", "ambipositions", "articles", "circumfixes", "circumpositions", "classifiers", "cmavo", "cmavo clusters", "cmene", "combining forms", "conjunctions", "counters", "determiners", "diacritical marks", "digraphs", "equative adjectives", "fu'ivla", "gismu", "Han characters", "Han tu", "hanja", "hanzi", "ideophones", "idioms", "infixes", "initialisms", "iteration marks", "interfixes", "interjections", "kana", "kanji", "letters", "ligatures", "logograms", "lujvo", "morae", "morphemes", "non-constituents", "nouns", "numbers", "numeral symbols", "numerals", "particles", "phrases", "postpositions", "postpositional phrases", "predicatives", "prefixes", "prepositional phrases", "prepositions", "preverbs", "pronominal adverbs", "pronouns", "proper nouns", "proverbs", "punctuation marks", "relatives", "roots", "stems", "suffixes", "syllables", "symbols", "verbs", } --[==[ var: Recognized non-lemma forms. If the part of speech in {{tl|head}} is set to one of these or its singular equivalent, the category 'LANG non-lemma forms' will automatically be added. If the part of speech is not a singular or plural lemma or non-lemma form and is not an abbreviation that expands to a recognized lemma or non-lemma form, the page will be added to various tracking categories; see the documentation of `data.lemmas`. ]==] data.nonlemmas = list_to_set{ "active participle forms", "active participles", "adjectival participles", "adjective case forms", "adjective forms", "adjective feminine forms", "adjective plural forms", "adverb forms", "adverbial participles", "agent participles", "article forms", "circumfix forms", "combined forms", "comparative adjective forms", "comparative adjectives", "comparative adverb forms", "comparative adverbs", "conjunction forms", "contractions", "converbs", "determiner comparative forms", "determiner forms", "determiner superlative forms", "diminutive nouns", "elative adjectives", "equative adjective forms", "equative adjectives", "future participles", "gerunds", "infinitive forms", "infinitives", "interjection forms", "jyutping", "misspellings", "negative participles", "nominal participles", "noun case forms", "noun construct forms", "noun dual forms", "noun forms", "noun paucal forms", "noun plural forms", "noun possessive forms", "noun singulative forms", "numeral forms", "participles", "participle forms", "particle forms", "passive participles", "past active participles", "past adverbial participles", "past participles", "past participle forms", "past passive participles", "perfect active participles", "perfect participles", "perfect passive participles", "pinyin", "plurals", "postposition forms", "prefix forms", "preposition contractions", "preposition forms", "prepositional pronouns", "present active participles", "present adverbial participles", "present participles", "present passive participles", "preverb forms", "pronoun forms", "pronoun possessive forms", "proper noun forms", "proper noun plural forms", "rafsi", "romanizations", "root forms", "singulatives", "suffix forms", "superlative adjective forms", "superlative adjectives", "superlative adverb forms", "superlative adverbs", "verb forms", "verbal nouns", } --[==[ var: List of languages that will not have links to separate parts of the headword. ]==] data.no_multiword_links = list_to_set{ "zh", } --[==[ var: List of languages that will not have `LANG multiword terms` categories added. There are various reasons why languages are in this list: (a) words are written without spaces between them; (b) syllables are written with spaces between them; (c) variant reconstructions are notated with a tilde surrounded by spaces; (d) the language is a sign language, where pagenames are multiword descriptions of the gesture(s) required to make an individual sign; (e) some other weirdnesses. ]==] data.no_multiword_cat = list_to_set{ -------- Languages without spaces between words (sometimes spaces between phrases) -------- "blt", -- Tai Dam "ja", -- Japanese "khb", -- Lü "km", -- Khmer "lo", -- Lao "mnw", -- Mon "my", -- Burmese "nan", -- Min Nan (some words in Latin script; hyphens between syllables) "nan-hbl", -- Hokkien (some words in Latin script; hyphens between syllables) "nod", -- Northern Thai "ojp", -- Old Japanese "shn", -- Shan "sou", -- Southern Thai "tdd", -- Tai Nüa "th", -- Thai "tts", -- Isan "twh", -- Tai Dón "txg", -- Tangut "zh", -- Chinese (all varieties with Chinese characters) "zkt", -- Khitan -------- Languages with spaces between syllables -------- "ahk", -- Akha "aou", -- A'ou "atb", -- Zaiwa "byk", -- Biao "cdy", -- Chadong --"duu", -- Drung; not sure --"hmx-pro", -- Proto-Hmong-Mien --"hnj", -- Green Hmong; not sure "huq", -- Tsat "ium", -- Iu Mien --"lis", -- Lisu; not sure "mtq", -- Muong --"mww", -- White Hmong; not sure "onb", -- Lingao --"sit-gkh", -- Gokhy; not sure --"swi", -- Sui; not sure "tbq-lol-pro", -- Proto-Loloish "tdh", -- Thulung "ukk", -- Muak Sa-aak "vi", -- Vietnamese "yig", -- Wusa Nasu "zng", -- Mang -------- Languages with ~ with surrounding spaces used to separate variants -------- "mkh-ban-pro", -- Proto-Bahnaric "sit-pro", -- Proto-Sino-Tibetan; listed above -------- Other weirdnesses -------- "mul", -- Translingual; gestures, Morse code, etc. "aot", -- Atong (India); bullet is a letter -------- All sign languages -------- "ads", "aed", "aen", "afg", "ase", "asf", "asp", "asq", "asw", "bfi", "bfk", "bog", "bqn", "bqy", "bvl", "bzs", "cds", "csc", "csd", "cse", "csf", "csg", "csl", "csn", "csq", "csr", "doq", "dse", "dsl", "ecs", "esl", "esn", "eso", "eth", "fcs", "fse", "fsl", "fss", "gds", "gse", "gsg", "gsm", "gss", "gus", "hab", "haf", "hds", "hks", "hos", "hps", "hsh", "hsl", "icl", "iks", "ils", "inl", "ins", "ise", "isg", "isr", "jcs", "jhs", "jls", "jos", "jsl", "jus", "kgi", "kvk", "lbs", "lls", "lsl", "lso", "lsp", "lst", "lsy", "lws", "mdl", "mfs", "mre", "msd", "msr", "mzc", "mzg", "mzy", "nbs", "ncs", "nsi", "nsl", "nsp", "nsr", "nzs", "okl", "pgz", "pks", "prl", "prz", "psc", "psd", "psg", "psl", "pso", "psp", "psr", "pys", "rms", "rsl", "rsm", "sdl", "sfb", "sfs", "sgg", "sgx", "slf", "sls", "sqk", "sqs", "ssp", "ssr", "svk", "swl", "syy", "tse", "tsm", "tsq", "tss", "tsy", "tza", "ugn", "ugy", "ukl", "uks", "vgt", "vsi", "vsl", "vsv", "xki", "xml", "xms", "ygs", "ysl", "zib", "zsl", } --[==[ var: List of languages where a hyphen is not considered a word separator for the `LANG multiword terms` category. There are numerous reasons why languages are in this list; by each language should be listed the reason for inclusion. ]==] data.hyphen_not_multiword_sep = list_to_set{ "akk", -- Akkadian; hyphens between syllables "akl", -- Aklanon; hyphens for mid-word glottal stops "ber-pro", -- Proto-Berber; morphemes separated by hyphens "ceb", -- Cebuano; hyphens for mid-word glottal stops "cnk", -- Khumi Chin; hyphens used in single words "cpi", -- Chinese Pidgin English; Chinese-derived words with hyphens between syllables "de", -- German; too many false positives "esx-esk-pro", -- hyphen used to separate morphemes "fi", -- Finnish; hyphen used to separate components in compound words if the final and initial vowels match, respectively "gd", -- Scottish Gaelic; too many false positives like [[a-chianaibh]], [[a-nìos]], [[an-dè]] and other adverbs in a- and an- "hil", -- Hiligaynon; hyphens for mid-word glottal stops "hnn", -- Hanunoo; too many false positives "ilo", -- Ilocano; hyphens for mid-word glottal stops "kne", -- Kankanaey; hyphens for mid-word glottal stops "lcp", -- Western Lawa; dash as syllable joiner "lwl", -- Eastern Lawa; dash as syllable joiner "mfa", -- Pattani Malay in Thai script; dash as syllable joiner "mkh-vie-pro", -- Proto-Vietic; morphemes separated by hyphens "msb", -- Masbatenyo; too many false positives "tl", -- Tagalog; too many false positives "war", -- Waray-Waray; too many false positives "yo", -- Yoruba; hyphens used to show lengthened nasal vowels } --[==[ var: List of languages that will not have `LANG masculine nouns` and similar categories added. Generally, these languages are lacking gender but use the gender field for other purposes. (This is a massive hack and should be changed.) ]==] data.no_gender_cat = list_to_set{ -- Languages without gender but which use the gender field for other purposes "ja", "th", } --[==[ var: List of languages where [[Module:headword]] should not attempt to generate a transliteration even if the term is written in a non-Latin script. FIXME: Notate reasons why each language is in this list. ]==] data.notranslit = list_to_set{ "ams", "az", "bbc", "bug", "cdo", "cia", "cjm", "cjy", "cmn", "cnp", "cpi", "cpx", "csp", "czh", "czo", "gan", "hak", "hnm", "hsn", "ja", "kzg", "lad", "ltc", "luh", "lzh", "mnp", "ms", "mul", "mvi", "nan", "nan-dat", "nan-hbl", "nan-hlh", "nan-lnx", "nan-tws", "nan-zhe", "nan-zsh", "och", "oj", "okn", "ryn", "rys", "ryu", "sh", "sjc", "tgt", "th", "tkn", "tly", "txg", "und", "vi", "wuu", "xug", "yoi", "yox", "yue", "za", "zh", "zhx-sic", "zhx-tai", } --[==[ var: List of languages that will default to `sccat` being true, i.e. categories like `LANG POS in SCRIPT script` will automatically be generated. This can be overridden using {{para|sccat|0}} in {{tl|head}} or setting `sccat` to `false` in Lua. ]==] data.default_sccat = list_to_set{ "inc-apa", "inc-ash", "kfr", "ks", "mr", "mwr", "inc-oaw", "inc-ohi", "omr", "inc-opa", "phr", "pi", "pra", "sa", "skr", "sd", } --[==[ var: List of script codes for which a script-tagged display title will be added. ]==] data.toBeTagged = list_to_set{ "Ahom", "Arab", "fa-Arab", "glk-Arab", "kk-Arab", "ks-Arab", "ku-Arab", "mzn-Arab", "ms-Arab", "ota-Arab", "pa-Arab", "ps-Arab", "sd-Arab", "tt-Arab", "ug-Arab", "ur-Arab", "Armi", "Armn", "Avst", "Bali", "Bamu", "Batk", "Beng", "as-Beng", "Bopo", "Brah", "Brai", "Bugi", "Buhd", "Cakm", "Cans", "Cari", "Cham", "Cher", "Copt", "Cprt", "Cyrl", "Cyrs", "Deva", "Dsrt", "Egyd", "Egyp", "Ethi", "Geok", "Geor", "Glag", "Goth", "Grek", "Polyt", "polytonic", "Gujr", "Guru", "Hang", "Hani", "Hano", "Hebr", "Hira", "Hluw", "Ital", "Java", "Kali", "Kana", "Khar", "Khmr", "Knda", "Kthi", "Lana", "Laoo", "Latn", "Latf", "Latg", "Latnx", "Latinx", "pjt-Latn", "Lepc", "Limb", "Linb", "Lisu", "Lyci", "Lydi", "Mand", "Mani", "Marc", "Merc", "Mero", "Mlym", "Mong", "mnc-Mong", "sjo-Mong", "xwo-Mong", "Mtei", "Mymr", "Narb", "Nkoo", "Nshu", "Ogam", "Olck", "Orkh", "Orya", "Osma", "Ougr", "Palm", "Phag", "Phli", "Phlv", "Phnx", "Plrd", "Prti", "Rjng", "Runr", "Samr", "Sarb", "Saur", "Sgnw", "Shaw", "Shrd", "Sinh", "Sora", "Sund", "Sylo", "Syrc", "Tagb", "Tale", "Talu", "Taml", "Tang", "Tavt", "Telu", "Tfng", "Tglg", "Thaa", "Thai", "Tibt", "Ugar", "Vaii", "Xpeo", "Xsux", "Yiii", "Zmth", "Zsym", "Ipach", "Music", "Rumin", } --[==[ var: Parts of speech which will not be categorised in categories like `English terms spelled with É` if the term is the character in question (e.g. the letter entry for English [[é]]). This contrasts with entries like the French adjective [[m̂]], which is a one-letter word spelled with the letter. ]==] data.pos_not_spelled_with_self = list_to_set{ "diacritical marks", "Han characters", "Han tu", "hanja", "hanzi", "iteration marks", "kana", "kanji", "letters", "ligatures", "logograms", "morae", "numeral symbols", "numerals", "punctuation marks", "syllables", "symbols", } ------ 2. Lists not converted into sets. ------ --[==[ var: Recognized aliases for parts of speech (param 2=). Key is the short form and value is the canonical singular (not pluralized) form. It is singular so the same table can be used in [[Module:form of]] for the {{para|p}}/{{para|POS}} param and [[Module:links]] for the pos= param. Note that any part of speech, abbreviated or not, can be suffixed with `f` to generate the corresponding non-lemma form part of speech, such as `adjf`, `af` or `adjectivef` for `adjective form`, and `nounf` or `nf` for `noun form`. This expansion happens even when it does not make sense for the given part of speech (e.g. `pclf` expands to `particle form` and `symf` expands to `symbol form`), and currently also, at least in [[Module:headword]] (but not [[Module:links]]), even if the part before the `f` is not a recognized part of speech or abbreviation (hence `nerf` expands to `ner form`). ]==] data.pos_aliases = { a = "adjective", adj = "adjective", adv = "adverb", art = "article", aug = "augmentative", cls = "classifier", compadj = "comparative adjective", compadv = "comparative adverb", compdet = "comparative determiner", comppron = "comparative pronoun", conj = "conjunction", contr = "contraction", conv = "converb", det = "determiner", dim = "diminutive", int = "interjection", interj = "interjection", intj = "interjection", n = "noun", -- the next two support Algonquian languages; see also vii/vai/vti/vta below na = "animate noun", ni = "inanimate noun", num = "numeral", part = "participle", pastpart = "past participle", pastptcp = "past participle", pcl = "particle", phr = "phrase", pn = "proper noun", postp = "postposition", pref = "prefix", prep = "preposition", prepphr = "prepositional phrase", prespart = "present participle", presptcp = "present participle", pron = "pronoun", prop = "proper noun", proper = "proper noun", propn = "proper noun", ptcp = "participle", rom = "romanization", roman = "romanization", romanisation = "romanization", romanisations = "romanization", suf = "suffix", supadj = "superlative adjective", supadv = "superlative adverb", supdet = "superlative determiner", suppron = "superlative pronoun", sym = "symbol", v = "verb", vb = "verb", vi = "intransitive verb", vm = "modal verb", vt = "transitive verb", -- the next four support Algonquian languages vii = "inanimate intransitive verb", vai = "animate intransitive verb", vti = "transitive inanimate verb", vta = "transitive animate verb", } --[==[ var: Map of parts of speech for which categories like `German masculine nouns` or `Russian imperfective verbs` will be generated if the headword is of the appropriate gender/number. The map is used to canonicalize parts of speech for categorization purposes; specifically, proper nouns categorizes like nouns. ]==] data.pos_for_gender_number_cat = { ["nouns"] = "nouns", ["proper nouns"] = "nouns", ["suffixes"] = "suffixes", -- We include verbs because impf and pf are valid "genders". ["verbs"] = "verbs", } --[==[ var: Lower limit for a "long" word in a particular language. Used to categorize terms into e.g. [[:Category:Long English words]] automatically. Languages with no mapping here do not get categorized. ]==] data.long_word_thresholds = { ["af"] = 20, ["bg"] = 20, ["cy"] = 25, ["de"] = 20, ["en"] = 25, ["es"] = 20, ["fr"] = 20, ["ka"] = 20, ["sv"] = 20, ["tl"] = 25, } ------ 3. Page-wide processing (so that it only needs to be done once per page). ------ data.page = require(headword_page_module).process_page() -- Set some page properties directly on `data` for ease of use. data.pagename = data.page.pagename data.encoded_pagename = data.page.encoded_pagename return data 81blund1lug5e3dv927kffaobiikgrc Mòideal:headword/page 828 16653 86138 2026-07-17T03:32:35Z Altronic 4137 Copy from English Wiktionary 86138 Scribunto text/plain local export = {} local languages_module = "Module:languages" local maintenance_category_module = "Module:maintenance category" local pages_module = "Module:pages" local string_compare_module = "Module:string/compare" local string_decode_entities_module = "Module:string/decodeEntities" local string_remove_comments_module = "Module:string/removeComments" local string_utilities_module = "Module:string utilities" local table_module = "Module:table" local template_parser_module = "Module:template parser" local mw = mw local string = string local table = table local ustring = mw.ustring local concat = table.concat local find = string.find local format = string.format local gsub = string.gsub local insert = table.insert local load_data = mw.loadData local match = string.match local new_title = mw.title.new local pairs = pairs local require = require local sub = string.sub local toNFC = ustring.toNFC local toNFD = ustring.toNFD local ugsub = ustring.gsub local function class_else_type(...) class_else_type = require(template_parser_module).class_else_type return class_else_type(...) end local function decode_entities(...) decode_entities = require(string_decode_entities_module) return decode_entities(...) end local function encode_entities(...) encode_entities = require(string_utilities_module).encode_entities return encode_entities(...) end local function get_category(...) get_category = require(maintenance_category_module).get_category return get_category(...) end local function get_lang(...) get_lang = require(languages_module).getByCode return get_lang(...) end local function list_to_set(...) list_to_set = require(table_module).listToSet return list_to_set(...) end local function parse(...) parse = require(template_parser_module).parse return parse(...) end local function remove_comments(...) remove_comments = require(string_remove_comments_module) return remove_comments(...) end local function physical_to_logical_pagename_if_mammoth(...) physical_to_logical_pagename_if_mammoth = require(pages_module).physical_to_logical_pagename_if_mammoth return physical_to_logical_pagename_if_mammoth(...) end local function split(...) split = require(string_utilities_module).split return split(...) end local function string_compare(...) string_compare = require(string_compare_module) return string_compare(...) 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 langnames local function get_langnames() langnames, get_langnames = load_data("Module:languages/canonical names"), nil return langnames end -- Combining character data used when categorising unusual characters. These resolve into two patterns, used to find -- single combining characters (i.e. character + diacritic(s)) or double combining characters (i.e. character + -- diacritic(s) + character). -- Charsets are in the format used by Unicode's UnicodeSet tool: https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp. -- Single combining characters. -- Charset: [[:M:]&[:^Canonical_Combining_Class=/^Double_/:]&[:^subhead=Grapheme joiner:]&[:^Variation_Selector=Yes:]] -- Note: concatenating hundreds of lines at once gives an error, so () are used every 150 lines to break it up into chunks. local comb_chars_single = ("\204\128-\205\142" .. -- U+0300-U+034E "\205\144-\205\155" .. -- U+0350-U+035B "\205\163-\205\175" .. -- U+0363-U+036F "\210\131-\210\137" .. -- U+0483-U+0489 "\214\145-\214\189" .. -- U+0591-U+05BD "\214\191" .. -- U+05BF "\215\129" .. -- U+05C1 "\215\130" .. -- U+05C2 "\215\132" .. -- U+05C4 "\215\133" .. -- U+05C5 "\215\135" .. -- U+05C7 "\216\144-\216\154" .. -- U+0610-U+061A "\217\139-\217\159" .. -- U+064B-U+065F "\217\176" .. -- U+0670 "\219\150-\219\156" .. -- U+06D6-U+06DC "\219\159-\219\164" .. -- U+06DF-U+06E4 "\219\167" .. -- U+06E7 "\219\168" .. -- U+06E8 "\219\170-\219\173" .. -- U+06EA-U+06ED "\220\145" .. -- U+0711 "\220\176-\221\138" .. -- U+0730-U+074A "\222\166-\222\176" .. -- U+07A6-U+07B0 "\223\171-\223\179" .. -- U+07EB-U+07F3 "\223\189" .. -- U+07FD "\224\160\150-\224\160\153" .. -- U+0816-U+0819 "\224\160\155-\224\160\163" .. -- U+081B-U+0823 "\224\160\165-\224\160\167" .. -- U+0825-U+0827 "\224\160\169-\224\160\173" .. -- U+0829-U+082D "\224\161\153-\224\161\155" .. -- U+0859-U+085B "\224\162\151-\224\162\159" .. -- U+0897-U+089F "\224\163\138-\224\163\161" .. -- U+08CA-U+08E1 "\224\163\163-\224\164\131" .. -- U+08E3-U+0903 "\224\164\186-\224\164\188" .. -- U+093A-U+093C "\224\164\190-\224\165\143" .. -- U+093E-U+094F "\224\165\145-\224\165\151" .. -- U+0951-U+0957 "\224\165\162" .. -- U+0962 "\224\165\163" .. -- U+0963 "\224\166\129-\224\166\131" .. -- U+0981-U+0983 "\224\166\188" .. -- U+09BC "\224\166\190-\224\167\132" .. -- U+09BE-U+09C4 "\224\167\135" .. -- U+09C7 "\224\167\136" .. -- U+09C8 "\224\167\139-\224\167\141" .. -- U+09CB-U+09CD "\224\167\151" .. -- U+09D7 "\224\167\162" .. -- U+09E2 "\224\167\163" .. -- U+09E3 "\224\167\190" .. -- U+09FE "\224\168\129-\224\168\131" .. -- U+0A01-U+0A03 "\224\168\188" .. -- U+0A3C "\224\168\190-\224\169\130" .. -- U+0A3E-U+0A42 "\224\169\135" .. -- U+0A47 "\224\169\136" .. -- U+0A48 "\224\169\139-\224\169\141" .. -- U+0A4B-U+0A4D "\224\169\145" .. -- U+0A51 "\224\169\176" .. -- U+0A70 "\224\169\177" .. -- U+0A71 "\224\169\181" .. -- U+0A75 "\224\170\129-\224\170\131" .. -- U+0A81-U+0A83 "\224\170\188" .. -- U+0ABC "\224\170\190-\224\171\133" .. -- U+0ABE-U+0AC5 "\224\171\135-\224\171\137" .. -- U+0AC7-U+0AC9 "\224\171\139-\224\171\141" .. -- U+0ACB-U+0ACD "\224\171\162" .. -- U+0AE2 "\224\171\163" .. -- U+0AE3 "\224\171\186-\224\171\191" .. -- U+0AFA-U+0AFF "\224\172\129-\224\172\131" .. -- U+0B01-U+0B03 "\224\172\188" .. -- U+0B3C "\224\172\190-\224\173\132" .. -- U+0B3E-U+0B44 "\224\173\135" .. -- U+0B47 "\224\173\136" .. -- U+0B48 "\224\173\139-\224\173\141" .. -- U+0B4B-U+0B4D "\224\173\149-\224\173\151" .. -- U+0B55-U+0B57 "\224\173\162" .. -- U+0B62 "\224\173\163" .. -- U+0B63 "\224\174\130" .. -- U+0B82 "\224\174\190-\224\175\130" .. -- U+0BBE-U+0BC2 "\224\175\134-\224\175\136" .. -- U+0BC6-U+0BC8 "\224\175\138-\224\175\141" .. -- U+0BCA-U+0BCD "\224\175\151" .. -- U+0BD7 "\224\176\128-\224\176\132" .. -- U+0C00-U+0C04 "\224\176\188" .. -- U+0C3C "\224\176\190-\224\177\132" .. -- U+0C3E-U+0C44 "\224\177\134-\224\177\136" .. -- U+0C46-U+0C48 "\224\177\138-\224\177\141" .. -- U+0C4A-U+0C4D "\224\177\149" .. -- U+0C55 "\224\177\150" .. -- U+0C56 "\224\177\162" .. -- U+0C62 "\224\177\163" .. -- U+0C63 "\224\178\129-\224\178\131" .. -- U+0C81-U+0C83 "\224\178\188" .. -- U+0CBC "\224\178\190-\224\179\132" .. -- U+0CBE-U+0CC4 "\224\179\134-\224\179\136" .. -- U+0CC6-U+0CC8 "\224\179\138-\224\179\141" .. -- U+0CCA-U+0CCD "\224\179\149" .. -- U+0CD5 "\224\179\150" .. -- U+0CD6 "\224\179\162" .. -- U+0CE2 "\224\179\163" .. -- U+0CE3 "\224\179\179" .. -- U+0CF3 "\224\180\128-\224\180\131" .. -- U+0D00-U+0D03 "\224\180\187" .. -- U+0D3B "\224\180\188" .. -- U+0D3C "\224\180\190-\224\181\132" .. -- U+0D3E-U+0D44 "\224\181\134-\224\181\136" .. -- U+0D46-U+0D48 "\224\181\138-\224\181\141" .. -- U+0D4A-U+0D4D "\224\181\151" .. -- U+0D57 "\224\181\162" .. -- U+0D62 "\224\181\163" .. -- U+0D63 "\224\182\129-\224\182\131" .. -- U+0D81-U+0D83 "\224\183\138" .. -- U+0DCA "\224\183\143-\224\183\148" .. -- U+0DCF-U+0DD4 "\224\183\150" .. -- U+0DD6 "\224\183\152-\224\183\159" .. -- U+0DD8-U+0DDF "\224\183\178" .. -- U+0DF2 "\224\183\179" .. -- U+0DF3 "\224\184\177" .. -- U+0E31 "\224\184\180-\224\184\186" .. -- U+0E34-U+0E3A "\224\185\135-\224\185\142" .. -- U+0E47-U+0E4E "\224\186\177" .. -- U+0EB1 "\224\186\180-\224\186\188" .. -- U+0EB4-U+0EBC "\224\187\136-\224\187\142" .. -- U+0EC8-U+0ECE "\224\188\152" .. -- U+0F18 "\224\188\153" .. -- U+0F19 "\224\188\181" .. -- U+0F35 "\224\188\183" .. -- U+0F37 "\224\188\185" .. -- U+0F39 "\224\188\190" .. -- U+0F3E "\224\188\191" .. -- U+0F3F "\224\189\177-\224\190\132" .. -- U+0F71-U+0F84 "\224\190\134" .. -- U+0F86 "\224\190\135" .. -- U+0F87 "\224\190\141-\224\190\151" .. -- U+0F8D-U+0F97 "\224\190\153-\224\190\188" .. -- U+0F99-U+0FBC "\224\191\134" .. -- U+0FC6 "\225\128\171-\225\128\190" .. -- U+102B-U+103E "\225\129\150-\225\129\153" .. -- U+1056-U+1059 "\225\129\158-\225\129\160" .. -- U+105E-U+1060 "\225\129\162-\225\129\164" .. -- U+1062-U+1064 "\225\129\167-\225\129\173" .. -- U+1067-U+106D "\225\129\177-\225\129\180" .. -- U+1071-U+1074 "\225\130\130-\225\130\141" .. -- U+1082-U+108D "\225\130\143" .. -- U+108F "\225\130\154-\225\130\157" .. -- U+109A-U+109D "\225\141\157-\225\141\159" .. -- U+135D-U+135F "\225\156\146-\225\156\149" .. -- U+1712-U+1715 "\225\156\178-\225\156\180" .. -- U+1732-U+1734 "\225\157\146" .. -- U+1752 "\225\157\147" .. -- U+1753 "\225\157\178" .. -- U+1772 "\225\157\179" .. -- U+1773 "\225\158\180-\225\159\147") .. -- U+17B4-U+17D3 ("\225\159\157" .. -- U+17DD "\225\162\133" .. -- U+1885 "\225\162\134" .. -- U+1886 "\225\162\169" .. -- U+18A9 "\225\164\160-\225\164\171" .. -- U+1920-U+192B "\225\164\176-\225\164\187" .. -- U+1930-U+193B "\225\168\151-\225\168\155" .. -- U+1A17-U+1A1B "\225\169\149-\225\169\158" .. -- U+1A55-U+1A5E "\225\169\160-\225\169\188" .. -- U+1A60-U+1A7C "\225\169\191" .. -- U+1A7F "\225\170\176-\225\171\142" .. -- U+1AB0-U+1ACE "\225\172\128-\225\172\132" .. -- U+1B00-U+1B04 "\225\172\180-\225\173\132" .. -- U+1B34-U+1B44 "\225\173\171-\225\173\179" .. -- U+1B6B-U+1B73 "\225\174\128-\225\174\130" .. -- U+1B80-U+1B82 "\225\174\161-\225\174\173" .. -- U+1BA1-U+1BAD "\225\175\166-\225\175\179" .. -- U+1BE6-U+1BF3 "\225\176\164-\225\176\183" .. -- U+1C24-U+1C37 "\225\179\144-\225\179\146" .. -- U+1CD0-U+1CD2 "\225\179\148-\225\179\168" .. -- U+1CD4-U+1CE8 "\225\179\173" .. -- U+1CED "\225\179\180" .. -- U+1CF4 "\225\179\183-\225\179\185" .. -- U+1CF7-U+1CF9 "\225\183\128-\225\183\140" .. -- U+1DC0-U+1DCC "\225\183\142-\225\183\187" .. -- U+1DCE-U+1DFB "\225\183\189-\225\183\191" .. -- U+1DFD-U+1DFF "\226\131\144-\226\131\176" .. -- U+20D0-U+20F0 "\226\179\175-\226\179\177" .. -- U+2CEF-U+2CF1 "\226\181\191" .. -- U+2D7F "\226\183\160-\226\183\191" .. -- U+2DE0-U+2DFF "\227\128\170-\227\128\175" .. -- U+302A-U+302F "\227\130\153" .. -- U+3099 "\227\130\154" .. -- U+309A "\234\153\175-\234\153\178" .. -- U+A66F-U+A672 "\234\153\180-\234\153\189" .. -- U+A674-U+A67D "\234\154\158" .. -- U+A69E "\234\154\159" .. -- U+A69F "\234\155\176" .. -- U+A6F0 "\234\155\177" .. -- U+A6F1 "\234\160\130" .. -- U+A802 "\234\160\134" .. -- U+A806 "\234\160\139" .. -- U+A80B "\234\160\163-\234\160\167" .. -- U+A823-U+A827 "\234\160\172" .. -- U+A82C "\234\162\128" .. -- U+A880 "\234\162\129" .. -- U+A881 "\234\162\180-\234\163\133" .. -- U+A8B4-U+A8C5 "\234\163\160-\234\163\177" .. -- U+A8E0-U+A8F1 "\234\163\191" .. -- U+A8FF "\234\164\166-\234\164\173" .. -- U+A926-U+A92D "\234\165\135-\234\165\147" .. -- U+A947-U+A953 "\234\166\128-\234\166\131" .. -- U+A980-U+A983 "\234\166\179-\234\167\128" .. -- U+A9B3-U+A9C0 "\234\167\165" .. -- U+A9E5 "\234\168\169-\234\168\182" .. -- U+AA29-U+AA36 "\234\169\131" .. -- U+AA43 "\234\169\140" .. -- U+AA4C "\234\169\141" .. -- U+AA4D "\234\169\187-\234\169\189" .. -- U+AA7B-U+AA7D "\234\170\176" .. -- U+AAB0 "\234\170\178-\234\170\180" .. -- U+AAB2-U+AAB4 "\234\170\183" .. -- U+AAB7 "\234\170\184" .. -- U+AAB8 "\234\170\190" .. -- U+AABE "\234\170\191" .. -- U+AABF "\234\171\129" .. -- U+AAC1 "\234\171\171-\234\171\175" .. -- U+AAEB-U+AAEF "\234\171\181" .. -- U+AAF5 "\234\171\182" .. -- U+AAF6 "\234\175\163-\234\175\170" .. -- U+ABE3-U+ABEA "\234\175\172" .. -- U+ABEC "\234\175\173" .. -- U+ABED "\239\172\158" .. -- U+FB1E "\239\184\160-\239\184\175" .. -- U+FE20-U+FE2F "\240\144\135\189" .. -- U+101FD "\240\144\139\160" .. -- U+102E0 "\240\144\141\182-\240\144\141\186" .. -- U+10376-U+1037A "\240\144\168\129-\240\144\168\131" .. -- U+10A01-U+10A03 "\240\144\168\133" .. -- U+10A05 "\240\144\168\134" .. -- U+10A06 "\240\144\168\140-\240\144\168\143" .. -- U+10A0C-U+10A0F "\240\144\168\184-\240\144\168\186" .. -- U+10A38-U+10A3A "\240\144\168\191" .. -- U+10A3F "\240\144\171\165" .. -- U+10AE5 "\240\144\171\166" .. -- U+10AE6 "\240\144\180\164-\240\144\180\167" .. -- U+10D24-U+10D27 "\240\144\181\169-\240\144\181\173" .. -- U+10D69-U+10D6D "\240\144\186\171" .. -- U+10EAB "\240\144\186\172" .. -- U+10EAC "\240\144\187\188-\240\144\187\191" .. -- U+10EFC-U+10EFF "\240\144\189\134-\240\144\189\144" .. -- U+10F46-U+10F50 "\240\144\190\130-\240\144\190\133" .. -- U+10F82-U+10F85 "\240\145\128\128-\240\145\128\130" .. -- U+11000-U+11002 "\240\145\128\184-\240\145\129\134" .. -- U+11038-U+11046 "\240\145\129\176" .. -- U+11070 "\240\145\129\179" .. -- U+11073 "\240\145\129\180" .. -- U+11074 "\240\145\129\191-\240\145\130\130" .. -- U+1107F-U+11082 "\240\145\130\176-\240\145\130\186" .. -- U+110B0-U+110BA "\240\145\131\130" .. -- U+110C2 "\240\145\132\128-\240\145\132\130" .. -- U+11100-U+11102 "\240\145\132\167-\240\145\132\180" .. -- U+11127-U+11134 "\240\145\133\133" .. -- U+11145 "\240\145\133\134" .. -- U+11146 "\240\145\133\179" .. -- U+11173 "\240\145\134\128-\240\145\134\130" .. -- U+11180-U+11182 "\240\145\134\179-\240\145\135\128" .. -- U+111B3-U+111C0 "\240\145\135\137-\240\145\135\140" .. -- U+111C9-U+111CC "\240\145\135\142" .. -- U+111CE "\240\145\135\143" .. -- U+111CF "\240\145\136\172-\240\145\136\183" .. -- U+1122C-U+11237 "\240\145\136\190" .. -- U+1123E "\240\145\137\129" .. -- U+11241 "\240\145\139\159-\240\145\139\170" .. -- U+112DF-U+112EA "\240\145\140\128-\240\145\140\131" .. -- U+11300-U+11303 "\240\145\140\187" .. -- U+1133B "\240\145\140\188" .. -- U+1133C "\240\145\140\190-\240\145\141\132" .. -- U+1133E-U+11344 "\240\145\141\135" .. -- U+11347 "\240\145\141\136" .. -- U+11348 "\240\145\141\139-\240\145\141\141" .. -- U+1134B-U+1134D "\240\145\141\151" .. -- U+11357 "\240\145\141\162" .. -- U+11362 "\240\145\141\163" .. -- U+11363 "\240\145\141\166-\240\145\141\172" .. -- U+11366-U+1136C "\240\145\141\176-\240\145\141\180" .. -- U+11370-U+11374 "\240\145\142\184-\240\145\143\128" .. -- U+113B8-U+113C0 "\240\145\143\130" .. -- U+113C2 "\240\145\143\133" .. -- U+113C5 "\240\145\143\135-\240\145\143\138" .. -- U+113C7-U+113CA "\240\145\143\140-\240\145\143\144" .. -- U+113CC-U+113D0 "\240\145\143\146" .. -- U+113D2 "\240\145\143\161" .. -- U+113E1 "\240\145\143\162" .. -- U+113E2 "\240\145\144\181-\240\145\145\134" .. -- U+11435-U+11446 "\240\145\145\158" .. -- U+1145E "\240\145\146\176-\240\145\147\131" .. -- U+114B0-U+114C3 "\240\145\150\175-\240\145\150\181" .. -- U+115AF-U+115B5 "\240\145\150\184-\240\145\151\128" .. -- U+115B8-U+115C0 "\240\145\151\156" .. -- U+115DC "\240\145\151\157" .. -- U+115DD "\240\145\152\176-\240\145\153\128" .. -- U+11630-U+11640 "\240\145\154\171-\240\145\154\183" .. -- U+116AB-U+116B7 "\240\145\156\157-\240\145\156\171" .. -- U+1171D-U+1172B "\240\145\160\172-\240\145\160\186" .. -- U+1182C-U+1183A "\240\145\164\176-\240\145\164\181" .. -- U+11930-U+11935 "\240\145\164\183" .. -- U+11937 "\240\145\164\184" .. -- U+11938 "\240\145\164\187-\240\145\164\190" .. -- U+1193B-U+1193E "\240\145\165\128") .. -- U+11940 ("\240\145\165\130" .. -- U+11942 "\240\145\165\131" .. -- U+11943 "\240\145\167\145-\240\145\167\151" .. -- U+119D1-U+119D7 "\240\145\167\154-\240\145\167\160" .. -- U+119DA-U+119E0 "\240\145\167\164" .. -- U+119E4 "\240\145\168\129-\240\145\168\138" .. -- U+11A01-U+11A0A "\240\145\168\179-\240\145\168\185" .. -- U+11A33-U+11A39 "\240\145\168\187-\240\145\168\190" .. -- U+11A3B-U+11A3E "\240\145\169\135" .. -- U+11A47 "\240\145\169\145-\240\145\169\155" .. -- U+11A51-U+11A5B "\240\145\170\138-\240\145\170\153" .. -- U+11A8A-U+11A99 "\240\145\176\175-\240\145\176\182" .. -- U+11C2F-U+11C36 "\240\145\176\184-\240\145\176\191" .. -- U+11C38-U+11C3F "\240\145\178\146-\240\145\178\167" .. -- U+11C92-U+11CA7 "\240\145\178\169-\240\145\178\182" .. -- U+11CA9-U+11CB6 "\240\145\180\177-\240\145\180\182" .. -- U+11D31-U+11D36 "\240\145\180\186" .. -- U+11D3A "\240\145\180\188" .. -- U+11D3C "\240\145\180\189" .. -- U+11D3D "\240\145\180\191-\240\145\181\133" .. -- U+11D3F-U+11D45 "\240\145\181\135" .. -- U+11D47 "\240\145\182\138-\240\145\182\142" .. -- U+11D8A-U+11D8E "\240\145\182\144" .. -- U+11D90 "\240\145\182\145" .. -- U+11D91 "\240\145\182\147-\240\145\182\151" .. -- U+11D93-U+11D97 "\240\145\187\179-\240\145\187\182" .. -- U+11EF3-U+11EF6 "\240\145\188\128" .. -- U+11F00 "\240\145\188\129" .. -- U+11F01 "\240\145\188\131" .. -- U+11F03 "\240\145\188\180-\240\145\188\186" .. -- U+11F34-U+11F3A "\240\145\188\190-\240\145\189\130" .. -- U+11F3E-U+11F42 "\240\145\189\154" .. -- U+11F5A "\240\147\145\128" .. -- U+13440 "\240\147\145\135-\240\147\145\149" .. -- U+13447-U+13455 "\240\150\132\158-\240\150\132\175" .. -- U+1611E-U+1612F "\240\150\171\176-\240\150\171\180" .. -- U+16AF0-U+16AF4 "\240\150\172\176-\240\150\172\182" .. -- U+16B30-U+16B36 "\240\150\189\143" .. -- U+16F4F "\240\150\189\145-\240\150\190\135" .. -- U+16F51-U+16F87 "\240\150\190\143-\240\150\190\146" .. -- U+16F8F-U+16F92 "\240\150\191\164" .. -- U+16FE4 "\240\150\191\176" .. -- U+16FF0 "\240\150\191\177" .. -- U+16FF1 "\240\155\178\157" .. -- U+1BC9D "\240\155\178\158" .. -- U+1BC9E "\240\156\188\128-\240\156\188\173" .. -- U+1CF00-U+1CF2D "\240\156\188\176-\240\156\189\134" .. -- U+1CF30-U+1CF46 "\240\157\133\165-\240\157\133\169" .. -- U+1D165-U+1D169 "\240\157\133\173-\240\157\133\178" .. -- U+1D16D-U+1D172 "\240\157\133\187-\240\157\134\130" .. -- U+1D17B-U+1D182 "\240\157\134\133-\240\157\134\139" .. -- U+1D185-U+1D18B "\240\157\134\170-\240\157\134\173" .. -- U+1D1AA-U+1D1AD "\240\157\137\130-\240\157\137\132" .. -- U+1D242-U+1D244 "\240\157\168\128-\240\157\168\182" .. -- U+1DA00-U+1DA36 "\240\157\168\187-\240\157\169\172" .. -- U+1DA3B-U+1DA6C "\240\157\169\181" .. -- U+1DA75 "\240\157\170\132" .. -- U+1DA84 "\240\157\170\155-\240\157\170\159" .. -- U+1DA9B-U+1DA9F "\240\157\170\161-\240\157\170\175" .. -- U+1DAA1-U+1DAAF "\240\158\128\128-\240\158\128\134" .. -- U+1E000-U+1E006 "\240\158\128\136-\240\158\128\152" .. -- U+1E008-U+1E018 "\240\158\128\155-\240\158\128\161" .. -- U+1E01B-U+1E021 "\240\158\128\163" .. -- U+1E023 "\240\158\128\164" .. -- U+1E024 "\240\158\128\166-\240\158\128\170" .. -- U+1E026-U+1E02A "\240\158\130\143" .. -- U+1E08F "\240\158\132\176-\240\158\132\182" .. -- U+1E130-U+1E136 "\240\158\138\174" .. -- U+1E2AE "\240\158\139\172-\240\158\139\175" .. -- U+1E2EC-U+1E2EF "\240\158\147\172-\240\158\147\175" .. -- U+1E4EC-U+1E4EF "\240\158\151\174" .. -- U+1E5EE "\240\158\151\175" .. -- U+1E5EF "\240\158\163\144-\240\158\163\150" .. -- U+1E8D0-U+1E8D6 "\240\158\165\132-\240\158\165\138") -- U+1E944-U+1E94A -- Double combining characters. -- Charset: [[:M:]&[:Canonical_Combining_Class=/^Double_/:]&[:^subhead=Grapheme joiner:]&[:^Variation_Selector=Yes:]] local comb_chars_double = "\205\156-\205\162" .. -- U+035C-U+0362 "\225\183\141" .. -- U+1DCD "\225\183\188" -- U+1DFC -- Variation selectors etc.; separated out so that we don't get categories for them. -- Charset: [[:M:]&[[:subhead=Grapheme joiner:][:Variation_Selector=Yes:]]]. local comb_chars_other = "\205\143" .. -- U+034F "\225\160\139-\225\160\141" .. -- U+180B-U+180D "\225\160\143" .. -- U+180F "\239\184\128-\239\184\143" .. -- U+FE00-U+FE0F "\243\160\132\128-\243\160\135\175" -- U+E0100-U+E01EF local comb_chars_all = comb_chars_single .. comb_chars_double .. comb_chars_other local comb_chars = { combined_single = "[^" .. comb_chars_all .. "][" .. comb_chars_single .. comb_chars_other .. "]+%f[^" .. comb_chars_all .. "]", combined_double = "[^" .. comb_chars_all .. "][" .. comb_chars_single .. comb_chars_other .. "]*[" .. comb_chars_double .. "]+[" .. comb_chars_all .. "]*.[" .. comb_chars_single .. comb_chars_other .. "]*", diacritics_single = "[" .. comb_chars_single .. "]", diacritics_double = "[" .. comb_chars_double .. "]", diacritics_all = "[" .. comb_chars_all .. "]" } -- Somewhat curated list from https://unicode.org/Public/emoji/16.0/emoji-sequences.txt. -- NOTE: There are lots more emoji sequences involving non-emoji Plane 0 symbols followed by 0xFE0F, which we don't -- (yet?) handle. local emoji_chars = "\226\140\154" .. -- U+231A (⌚) "\226\140\155" .. -- U+231B (⌛) "\226\140\168" .. -- U+2328 (⌨) "\226\143\143" .. -- U+23CF (⏏) "\226\143\169-\226\143\179" .. -- U+23E9-U+23F3 (⏩-⏳) "\226\143\184-\226\143\186" .. -- U+23F8-U+23FA (⏸-⏺) "\226\150\170" .. -- U+25AA (▪) "\226\150\171" .. -- U+25AB (▫) "\226\150\182" .. -- U+25B6 (▶) "\226\151\128" .. -- U+25C0 (◀) "\226\151\187-\226\151\190" .. -- U+25FB-U+25FE (◻-◾) "\226\152\128-\226\152\132" .. -- U+2600-U+2604 (☀-☄) "\226\152\142" .. -- U+260E (☎) "\226\152\145" .. -- U+2611 (☑) "\226\152\148" .. -- U+2614 (☔) "\226\152\149" .. -- U+2615 (☕) "\226\152\152" .. -- U+2618 (☘) "\226\152\157" .. -- U+261D (☝) "\226\152\160" .. -- U+2620 (☠) "\226\152\162" .. -- U+2622 (☢) "\226\152\163" .. -- U+2623 (☣) "\226\152\166" .. -- U+2626 (☦) "\226\152\170" .. -- U+262A (☪) "\226\152\174" .. -- U+262E (☮) "\226\152\175" .. -- U+262F (☯) "\226\152\184-\226\152\186" .. -- U+2638-U+263A (☸-☺) "\226\153\136-\226\153\147" .. -- U+2648-U+2653 (♈-♓) "\226\153\159" .. -- U+265F (♟) "\226\153\160" .. -- U+2660 (♠) "\226\153\163" .. -- U+2663 (♣) "\226\153\165" .. -- U+2665 (♥) "\226\153\166" .. -- U+2666 (♦) "\226\153\168" .. -- U+2668 (♨) "\226\153\187" .. -- U+267B (♻) "\226\153\190" .. -- U+267E (♾) "\226\153\191" .. -- U+267F (♿) "\226\154\146-\226\154\151" .. -- U+2692-U+2697 (⚒-⚗) "\226\154\153" .. -- U+2699 (⚙) "\226\154\155" .. -- U+269B (⚛) "\226\154\156" .. -- U+269C (⚜) "\226\154\160" .. -- U+26A0 (⚠) "\226\154\161" .. -- U+26A1 (⚡) "\226\154\170" .. -- U+26AA (⚪) "\226\154\171" .. -- U+26AB (⚫) "\226\154\176" .. -- U+26B0 (⚰) "\226\154\177" .. -- U+26B1 (⚱) "\226\154\189" .. -- U+26BD (⚽) "\226\154\190" .. -- U+26BE (⚾) "\226\155\132" .. -- U+26C4 (⛄) "\226\155\133" .. -- U+26C5 (⛅) "\226\155\136" .. -- U+26C8 (⛈) "\226\155\142" .. -- U+26CE (⛎) "\226\155\143" .. -- U+26CF (⛏) "\226\155\145" .. -- U+26D1 (⛑) "\226\155\147" .. -- U+26D3 (⛓) "\226\155\148" .. -- U+26D4 (⛔) "\226\155\169" .. -- U+26E9 (⛩) "\226\155\170" .. -- U+26EA (⛪) "\226\155\176-\226\155\181" .. -- U+26F0-U+26F5 (⛰-⛵) "\226\155\183-\226\155\186" .. -- U+26F7-U+26FA (⛷-⛺) "\226\155\189" .. -- U+26FD (⛽) "\226\156\130" .. -- U+2702 (✂) "\226\156\133" .. -- U+2705 (✅) "\226\156\136-\226\156\141" .. -- U+2708-U+270D (✈-✍) "\226\156\143" .. -- U+270F (✏) "\226\156\146" .. -- U+2712 (✒) "\226\156\148" .. -- U+2714 (✔) "\226\156\150" .. -- U+2716 (✖) "\226\156\157" .. -- U+271D (✝) "\226\156\161" .. -- U+2721 (✡) "\226\156\168" .. -- U+2728 (✨) "\226\156\179" .. -- U+2733 (✳) "\226\156\180" .. -- U+2734 (✴) "\226\157\132" .. -- U+2744 (❄) "\226\157\135" .. -- U+2747 (❇) "\226\157\140" .. -- U+274C (❌) "\226\157\142" .. -- U+274E (❎) "\226\157\147-\226\157\149" .. -- U+2753-U+2755 (❓-❕) "\226\157\151" .. -- U+2757 (❗) "\226\157\163" .. -- U+2763 (❣) "\226\157\164" .. -- U+2764 (❤) "\226\158\149-\226\158\151" .. -- U+2795-U+2797 (➕-➗) "\226\158\161" .. -- U+27A1 (➡) "\226\158\176" .. -- U+27B0 (➰) "\226\158\191" .. -- U+27BF (➿) "\226\164\180" .. -- U+2934 (⤴) "\226\164\181" .. -- U+2935 (⤵) "\226\172\133-\226\172\135" .. -- U+2B05-U+2B07 (⬅-⬇) "\226\172\155" .. -- U+2B1B (⬛) "\226\172\156" .. -- U+2B1C (⬜) "\226\173\144" .. -- U+2B50 (⭐) "\226\173\149" .. -- U+2B55 (⭕) "\227\128\176" .. -- U+3030 (〰) "\227\128\189" .. -- U+303D (〽) "\227\138\151" .. -- U+3297 (㊗) "\227\138\153" .. -- U+3299 (㊙) "\240\159\128\132" .. -- U+1F004 (🀄) "\240\159\131\143" .. -- U+1F0CF (🃏) "\240\159\133\176" .. -- U+1F170 (🅰) "\240\159\133\177" .. -- U+1F171 (🅱) "\240\159\133\190" .. -- U+1F17E (🅾) "\240\159\133\191" .. -- U+1F17F (🅿) "\240\159\134\142" .. -- U+1F18E (🆎) "\240\159\134\145-\240\159\134\154" .. -- U+1F191-U+1F19A (🆑-🆚) "\240\159\136\129" .. -- U+1F201 (🈁) "\240\159\136\130" .. -- U+1F202 (🈂) "\240\159\136\154" .. -- U+1F21A (🈚) "\240\159\136\175" .. -- U+1F22F (🈯) "\240\159\136\178-\240\159\136\186" .. -- U+1F232-U+1F23A (🈲-🈺) "\240\159\137\144" .. -- U+1F250 (🉐) "\240\159\137\145" .. -- U+1F251 (🉑) "\240\159\140\128-\240\159\153\143" .. -- U+1F300-U+1F64F (🌀-🙏) "\240\159\154\128-\240\159\155\151" .. -- U+1F680-U+1F6D7 (🚀-🛗) "\240\159\155\156-\240\159\155\172" .. -- U+1F6DC-U+1F6EC (🛜-🛬) "\240\159\155\176-\240\159\155\188" .. -- U+1F6F0-U+1F6FC (🛰-🛼) "\240\159\159\160-\240\159\159\171" .. -- U+1F7E0-U+1F7EB (🟠-🟫) "\240\159\159\176" .. -- U+1F7F0 (🟰) "\240\159\164\140-\240\159\169\147" .. -- U+1F90C-U+1FA53 (🤌-🩓) "\240\159\169\160-\240\159\169\173" .. -- U+1FA60-U+1FA6D (🩠-🩭) "\240\159\169\176-\240\159\169\188" .. -- U+1FA70-U+1FA7C (🩰-🩼) "\240\159\170\128-\240\159\170\137" .. -- U+1FA80-U+1FA89 (🪀-🪉) "\240\159\170\143-\240\159\171\134" .. -- U+1FA8F-U+1FAC6 (🪏-🫆) "\240\159\171\142-\240\159\171\156" .. -- U+1FACE-U+1FADC (🫎-🫜) "\240\159\171\159-\240\159\171\169" .. -- U+1FADF-U+1FAE9 (🫟-🫩) "\240\159\171\176-\240\159\171\184" -- U+1FAF0-U+1FAF8 (🫰-🫸) local unsupported_characters local function get_unsupported_characters() unsupported_characters, get_unsupported_characters = {}, nil for k, v in pairs(load_data("Module:links/data").unsupported_characters) do unsupported_characters[v] = k end return unsupported_characters end -- The list of unsupported titles and invert it (so the keys are pagenames and values are canonical titles). local unsupported_titles local function get_unsupported_titles() unsupported_titles, get_unsupported_titles = {}, nil for k, v in pairs(load_data("Module:links/data").unsupported_titles) do unsupported_titles[v] = k end return unsupported_titles end -- To save on memory, we only cache names with either non-ASCII characters in them or ASCII characters to be removed or -- transformed (apostrophe, double quote, hyphen). local L2_sort_key_cache = {} function export.get_L2_sort_key(L2) if L2 == "Translingual" then return "\1" elseif L2 == "English" then return "\2" elseif match(L2, "^[%z\1-\b\14-!#-&(-,.-\127]+$") then return L2 end local sort_key = L2_sort_key_cache[L2] if sort_key then return sort_key end sort_key = toNFC(ugsub(ugsub(toNFD(L2), "[" .. comb_chars_all .. "'\"ʻʼ]+", ""), "[%s%-]+", " ")) L2_sort_key_cache[L2] = sort_key return sort_key end --[==[ Given a pagename (or {nil} for the current page), create and return a data structure describing the page. The returned object includes the following fields: * `comb_chars`: A table containing various Lua character class patterns for different types of combined characters (those that decompose into multiple characters in the NFD decomposition). The patterns are meant to be used with {mw.ustring.find()}. The keys are: ** `single`: Single combining characters (character + diacritic), without surrounding brackets; ** `double`: Double combining characters (character + diacritic + character), without surrounding brackets; ** `vs`: Variation selectors, without surrounding brackets; ** `all`: Concatenation of `single` + `double` + `vs`, without surrounding brackets; ** `diacritics_single`: Like `single` but with surrounding brackets; ** `diacritics_double`: Like `double` but with surrounding brackets; ** `diacritics_all`: Like `all` but with surrounding brackets; ** `combined_single`: Lua pattern for matching a spacing character followed by one or more single combining characters; ** `combined_double`: Lua pattern for matching a combination of two spacing characters separated by one or more double combining characters, possibly also with single combining characters; * `emoji_pattern`: A Lua character class pattern (including surrounding brackets) that matches emojis. Meant to be used with {mw.ustring.find()}. * `L2_list`: Ordered list of L2 headings on the page, with the extra key `n` that gives the length of the list. * `L2_sections`: Lookup table of L2 headings on the page, where the key is the section number assigned by the preprocessor, and the value is the L2 heading name. Once an invocation has got its actual section number from get_current_L2 in [[Module:pages]], it can use this table to determine its parent L2. TODO: We could expand this to include subsections, to check POS headings are correct etc. * `unsupported_titles`: Map from pagenames to canonical titles for unsupported-title pages. * `namespace`: Namespace of the pagename. * `ns`: Namespace table for the page from mw.site.namespaces (TODO: merge with `namespace` above). * `full_raw_pagename`: Full version of the '''RAW''' pagename (i.e. unsupported-title pages aren't canonicalized); including the namespace and the base (portion before the slash). * `pagename`: Canonicalized subpage portion of the pagename (unsupported-title pages are canonicalized). * `pagename_with_base`: Same as `pagename` in the main namespace; otherwise, the whole pagename without the namespace. * `decompose_pagename`: Equivalent of `pagename` in NFD decomposition. * `pagename_len`: Length of `pagename` in Unicode chars, where combinations of spacing character + decomposed diacritic are treated as single characters. * `explode_pagename`: Set of characters found in `pagename`. The keys are characters (where combinations of spacing character + decomposed diacritic are treated as single characters). * `encoded_pagename`: FIXME: Document me. * `pagename_defaultsort`: FIXME: Document me. * `raw_defaultsort`: FIXME: Document me. * `wikitext_topic_cat`: FIXME: Document me. * `wikitext_langname_cat`: FIXME: Document me. `no_fetch_content` says to not fetch and parse the content or set a DEFAULTSORT sort key, in order to save time on test and documentation pages that have lots of template invocations that set `|pagename=`. It turns out nearly all the time of this function is contained in the line `frame:callParserFunction("DEFAULTSORT", data.pagename_defaultsort)`, so we skip it on test and documentation pages where it accomplishes nothing in any case. ]==] function export.process_page(pagename, no_fetch_content) local data = { comb_chars = comb_chars, emoji_pattern = "[" .. emoji_chars .. "]", unsupported_titles = unsupported_titles or get_unsupported_titles() } local cats = {} data.cats = cats -- We cannot store `raw_title` in `data` because it contains a metatable. local raw_title local function bad_pagename() if not pagename then error("Internal error: Something wrong, `data.pagename` not specified but current title contains illegal characters") else error(format("Bad value for `data.pagename`: '%s', which must not contain illegal characters", pagename)) end end if pagename then -- for testing, doc pages, etc. raw_title = new_title(pagename) if not raw_title then bad_pagename() end else raw_title = mw.title.getCurrentTitle() end local nsText = raw_title.nsText local namespace_is_reconstruction = nsText == "Reconstruction" data.namespace = nsText data.ns = mw.site.namespaces[raw_title.namespace] local full_raw_pagename = raw_title.fullText data.full_raw_pagename = full_raw_pagename local frame = mw.getCurrentFrame() -- WARNING: `content` may be nil, e.g. if we're substing a template like {{ja-new}} on a not-yet-created page -- or if the module specifies the subpage as `data.pagename` (which many modules do) and we're in an Appendix -- or other non-mainspace page. We used to make the latter an error but there are too many modules that do it, -- and substing on a nonexistent page is totally legit, and we don't actually need to be able to access the -- content of the page. local content = not no_fetch_content and raw_title:getContent() or nil -- Get the pagename. pagename = physical_to_logical_pagename_if_mammoth(raw_title) pagename = gsub(pagename, "^Unsupported titles/(.+)", function(m) insert(cats, "Unsupported titles") local title = (unsupported_titles or get_unsupported_titles())[m] if title then return title end -- Substitute pairs of "`". Those not used for escaping should be escaped as "`grave`", but might not be, -- so if a pair don't form a match, the closing "`" should become the opening "`" of the next match attempt. -- This has to be done manually, instead of using gsub. local open_pos = find(m, "`") if not open_pos then return m end title = {sub(m, 1, open_pos - 1)} while true do local close_pos = find(m, "`", open_pos + 1) if not close_pos then -- Add "`" plus any remaining characters. insert(title, sub(m, open_pos)) break end local escape = sub(m, open_pos, close_pos) local ch = (unsupported_characters or get_unsupported_characters())[escape] -- Match found, so substitute the character and move to the first "`" after the match if found, or -- otherwise return. if ch then insert(title, ch) local nxt_pos = close_pos + 1 open_pos = find(m, "`", nxt_pos) -- Add any characters between the match and the next "`" or end. if open_pos then insert(title, sub(m, nxt_pos, open_pos - 1)) else insert(title, sub(m, nxt_pos)) break end -- Match not found, so make the closing "`" the opening "`" of the next attempt. else -- Add the failed match, except for the closing "`". insert(title, sub(m, open_pos, close_pos - 1)) open_pos = close_pos end end return concat(title) end) -- Save pagename, as the local variable will be destructively modified. data.pagename = pagename if nsText == "" then data.pagename_with_base = pagename else data.pagename_with_base = raw_title.text end -- Decompose the pagename in Unicode normalization form D. data.decompose_pagename = toNFD(pagename) -- Explode the current page name into a character table, taking decomposed combining characters into account. local explode_pagename = {} local pagename_len = 0 local function explode(char) explode_pagename[char] = true pagename_len = pagename_len + 1 return "" end pagename = ugsub(pagename, comb_chars.combined_double, explode) pagename = gsub(ugsub(pagename, comb_chars.combined_single, explode), ".[\128-\191]*", explode) data.explode_pagename = explode_pagename data.pagename_len = pagename_len -- Generate DEFAULTSORT. data.encoded_pagename = encode_entities(data.pagename) data.pagename_defaultsort = get_lang("mul"):makeSortKey(data.encoded_pagename) if not no_fetch_content then frame:callParserFunction("DEFAULTSORT", data.pagename_defaultsort) end data.raw_defaultsort = uupper(raw_title.text) -- Make `L2_list` and `L2_sections`, note raw wikitext use of {{DEFAULTSORT:}} and {{DISPLAYTITLE:}}, then add categories if any unwanted L1 headings are found, the L2 headings are in the wrong order, or they don't match a canonical language name. -- Note: HTML comments shouldn't be removed from `content` until after this step, as they can affect the result. do local L2_list, L2_list_len, L2_sections = {}, 0, {} local prev, rc local new_cats, L2_wrong_order = {} local function handle_heading(heading) local level = heading.level if level > 2 then return end local name = heading:get_name() -- heading:get_name() will return nil if there are any newline characters in the preprocessed heading name (e.g. from an expanded template). In such cases, the preprocessor section count still increments (since it's calculated pre-expansion), but the heading will fail, so the L2 count shouldn't be incremented. if name == nil then return end L2_list_len = L2_list_len + 1 L2_list[L2_list_len] = name L2_sections[heading.section] = name -- Also add any L1s, since they terminate the preceding L2, but add a maintenance category since it's probably a mistake. if level == 1 then new_cats["Pages with unwanted L1 headings"] = true end -- Check the heading is in the right order. -- FIXME: we need a more sophisticated sorting method which handles non-diacritic special characters (e.g. Magɨ). if prev and not ( L2_wrong_order or string_compare(export.get_L2_sort_key(prev), export.get_L2_sort_key(name)) ) then new_cats["Pages with language headings in the wrong order"] = true L2_wrong_order = true end -- Check it's a canonical language name. if not (langnames or get_langnames())[name] then new_cats["Pages with nonstandard language headings"] = true end prev = name end local function handle_template(template) -- Turn off redirect checking except in the Reconstruction namespace because the rc flag is only -- used in the Reconstruction namespace and the other names are parser functions, which AFAIK can't -- be redirected to. local name = template:get_name(nil, not namespace_is_reconstruction and "no_redirect" or nil) if name == "DEFAULTSORT:" then new_cats["Pages with DEFAULTSORT conflicts"] = true elseif name == "DISPLAYTITLE:" then new_cats["Pages with DISPLAYTITLE conflicts"] = true elseif name == "reconstructed" then rc = true end end if content then for node in parse(content):iterate_nodes() do local node_class = class_else_type(node) if node_class == "heading" then handle_heading(node) elseif node_class == "template" then handle_template(node) elseif node_class == "parameter" then new_cats["Pages with raw triple-brace template parameters"] = true end end end L2_list.n = L2_list_len data.L2_list = L2_list data.L2_sections = L2_sections insert(cats, get_category("Pages with entries")) insert(cats, get_category(format("Pages with %s entr%s", L2_list_len, L2_list_len == 1 and "y" or "ies"))) for cat in pairs(new_cats) do insert(cats, get_category(cat)) end if namespace_is_reconstruction and not rc then local langname = match(full_raw_pagename, "^Reconstruction:([^/]+)/.") if langname then insert(cats, get_category(langname .. " entries missing Template:reconstructed")) end end end ------ 4. Parse page for maintenance categories. ------ -- Use of tab characters. if content and find(content, "\t", 1, true) then insert(cats, get_category("Pages with tab characters")) end -- Unencoded character(s) in title. local IDS = list_to_set{"⿰", "⿱", "⿲", "⿳", "⿴", "⿵", "⿶", "⿷", "⿸", "⿹", "⿺", "⿻", "⿼", "⿽", "⿾", "⿿", "㇯"} for char in pairs(explode_pagename) do if IDS[char] and char ~= data.pagename then insert(cats, "Terms containing unencoded characters") break end end -- Raw wikitext use of a topic or langname category. Also check if any raw sortkeys have been used. do local wikitext_topic_cat = {} local wikitext_langname_cat = {} local raw_sortkey -- If a raw sortkey has been found, add it to the relevant table. -- If there's no table (or the index is just `true`), create one first. local function add_cat_table(t, lang, sortkey) local t_lang = t[lang] if not sortkey then if not t_lang then t[lang] = true end return elseif t_lang == true or not t_lang then t_lang = {} t[lang] = t_lang end t_lang[uupper(decode_entities(sortkey))] = true end local function process_category(content, cat, colon, nxt) local pipe = find(cat, "|", colon + 1, true) -- Categories cannot end "|]]". if pipe == #cat then return end local title = new_title(pipe and sub(cat, 1, pipe - 1) or cat) if not (title and title.namespace == 14) then return end -- Get the sortkey (if any), then canonicalize category title. local sortkey = pipe and sub(cat, pipe + 1) or nil cat = title.text if sortkey then raw_sortkey = true -- If the sortkey contains "[", the first "]" of a final "]]]" is treated as part of the sortkey. if find(sortkey, "[", 1, true) and sub(content, nxt, nxt) == "]" then sortkey = sortkey .. "]" end end local code = match(cat, "^([%w%-.]+):") if code then add_cat_table(wikitext_topic_cat, code, sortkey) return end -- Split by word. cat = split(cat, " ", true, true) -- Formerly we looked for the language name anywhere in the category. This is simply wrong -- because there are no categories like 'Alsatian French lemmas' (only L2 languages -- have langname categories), but doing it this way wrongly catches things like [[Category:Shapsug Adyghe]] -- in [[Category:Adyghe entries with language name categories using raw markup]]. local n = #cat - 1 if n <= 0 then return end -- Go from longest to shortest and stop once we've found a language name. Going from shortest -- to longest or not stopping after a match risks falsely matching (e.g.) German Low German -- categories as German. repeat local name = concat(cat, " ", 1, n) if (langnames or get_langnames())[name] then add_cat_table(wikitext_langname_cat, name, sortkey) return end n = n - 1 until n == 0 end if content then -- Remove comments, then iterate over category links. content = remove_comments(content, "BOTH") local head = find(content, "[[", 1, true) while head do local close = find(content, "]]", head + 2, true) if not close then break end -- Make sure there are no intervening "[[" between head and close. local open = find(content, "[[", head + 2, true) while open and open < close do head = open open = find(content, "[[", head + 2, true) end local cat = sub(content, head + 2, close - 1) -- Locate the colon, and weed out most unwanted links. "[ _\128-\244]*" catches valid whitespace, and ensures any category links using the colon trick are ignored. We match all non-ASCII characters, as there could be multibyte spaces, and mw.title.new will filter out any remaining false-positives; this is a lot faster than running mw.title.new on every link. local colon = match(cat, "^[ _\128-\244]*[Cc][Aa][Tt][EeGgOoRrYy _\128-\244]*():") if colon then process_category(content, cat, colon, close + 2) end head = open end end data.wikitext_topic_cat = wikitext_topic_cat data.wikitext_langname_cat = wikitext_langname_cat if raw_sortkey then insert(cats, get_category("Pages with raw sortkeys")) end end return data end return export 0esb0zcz2sjpy8aqh1uv1deoe3q625h