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:form of
828
16706
86194
2026-07-19T23:13:37Z
Altronic
4137
Copy from English Wiktionary
86194
Scribunto
text/plain
local export = {}
export.force_cat = false -- for testing; set to true to display categories even on non-mainspace pages
local debug_track_module = "Module:debug/track"
local etymology_module = "Module:etymology"
local form_of_cats_module = "Module:form of/cats"
local form_of_data_module = "Module:form of/data"
local form_of_data1_module = "Module:form of/data/1"
local form_of_data2_module = "Module:form of/data/2"
local form_of_functions_module = "Module:form of/functions"
local form_of_lang_data_module_prefix = "Module:form of/lang-data/"
local function_module = "Module:fun"
local headword_data_module = "Module:headword/data"
local json_module = "Module:JSON"
local labels_module = "Module:labels"
local links_module = "Module:links"
local load_module = "Module:load"
local parse_utilities_module = "Module:parse utilities"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local table_deep_equals_module = "Module:table/deepEquals"
local utilities_module = "Module:utilities"
local anchor_encode = mw.uri.anchorEncode
local concat = table.concat
local dump = mw.dumpObject
local fetch_categories_and_labels -- Defined below.
local format_form_of -- Defined below.
local get_tag_display_form -- Defined below.
local get_tag_set_display_form -- Defined below.
local insert = table.insert
local ipairs = ipairs
local is_link_or_html -- Defined below.
local list_to_text = mw.text.listToText
local lookup_shortcut -- Defined below.
local lookup_tag -- Defined below.
local normalize_tag_set -- Defined below.
local parse_tag_set_properties -- Defined below.
local require = require
local sort = table.sort
local split_tag_set -- Defined below.
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 append(...)
append = require(table_module).append
return append(...)
end
local function contains(...)
contains = require(table_module).contains
return contains(...)
end
local function debug_track(...)
debug_track = require(debug_track_module)
return debug_track(...)
end
local function deep_copy(...)
deep_copy = require(table_module).deepCopy
return deep_copy(...)
end
local function deep_equals(...)
deep_equals = require(table_deep_equals_module)
return deep_equals(...)
end
local function extend(...)
extend = require(table_module).extend
return extend(...)
end
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function full_link(...)
full_link = require(links_module).full_link
return full_link(...)
end
local function insert_if_not(...)
insert_if_not = require(table_module).insertIfNot
return insert_if_not(...)
end
local function is_subset_list(...)
is_subset_list = require(table_module).isSubsetList
return is_subset_list(...)
end
local function iterate_from(...)
iterate_from = require(function_module).iterateFrom
return iterate_from(...)
end
local function join_multiparts(...)
join_multiparts = require(form_of_functions_module).join_multiparts
return join_multiparts(...)
end
local function load_data(...)
load_data = require(load_module).load_data
return load_data(...)
end
local function parse_inline_modifiers(...)
parse_inline_modifiers = require(parse_utilities_module).parse_inline_modifiers
return parse_inline_modifiers(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function safe_load_data(...)
safe_load_data = require(load_module).safe_load_data
return safe_load_data(...)
end
local function safe_require(...)
safe_require = require(load_module).safe_require
return safe_require(...)
end
local function serial_comma_join(...)
serial_comma_join = require(table_module).serialCommaJoin
return serial_comma_join(...)
end
local function shallow_copy(...)
shallow_copy = require(table_module).shallowCopy
return shallow_copy(...)
end
local function show_labels(...)
show_labels = require(labels_module).show_labels
return show_labels(...)
end
local function slice(...)
slice = require(table_module).slice
return slice(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function ucfirst(...)
ucfirst = require(string_utilities_module).ucfirst
return ucfirst(...)
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 cat_functions
local function get_cat_functions()
cat_functions, get_cat_functions = require(form_of_functions_module).cat_functions, nil
return cat_functions
end
local default_pagename
local function get_default_pagename()
default_pagename, get_default_pagename = load_data(headword_data_module).pagename, nil
return default_pagename
end
local display_handlers
local function get_display_handlers()
display_handlers, get_display_handlers = require(form_of_functions_module).display_handlers, nil
return display_handlers
end
local m_cats_data
local function get_m_cats_data()
m_cats_data, get_m_cats_data = load_data(form_of_cats_module), nil
return m_cats_data
end
local m_data
local function get_m_data()
-- Needs require.
m_data, get_m_data = require(form_of_data_module), nil
return m_data
end
local m_data1
local function get_m_data1()
m_data1, get_m_data1 = load_data(form_of_data1_module), nil
return m_data1
end
local m_data2
local function get_m_data2()
m_data2, get_m_data2 = load_data(form_of_data2_module), nil
return m_data2
end
local m_pos_data
local function get_m_pos_data()
m_pos_data, get_m_pos_data = load_data(headword_data_module).pos_aliases, nil
return m_pos_data
end
--[==[ intro:
This module implements the underlying processing of {{tl|form of}}, {{tl|inflection of}} and specific variants such as
{{tl|past participle of}} and {{tl|alternative spelling of}}. Most of the logic in this file is to handle tags in
{{tl|inflection of}}. Other related files:
* [[Module:form of/templates]] contains the majority of the logic that implements the templates themselves.
* [[Module:form of/data/1]] is a data-only file containing information on the more common inflection tags, listing the
tags, their shortcuts, the category they belong to (tense-aspect, case, gender, voice-valence, etc.), the appropriate
glossary link and the wikidata ID.
* [[Module:form of/data/2]] is a data-only file containing information on the less common inflection tags, in the same
format as [[Module:form of/data/1]].
* [[Module:form of/lang-data/LANGCODE]] is a data-only file containing information on the language-specific inflection
tags for the language with code LANGCODE, in the same format as [[Module:form of/data/1]]. Language-specific tags
override general tags.
* [[Module:form of/cats]] is a data-only file listing the language-specific categories that are added when the
appropriate combinations of tags are seen for a given language.
* [[Module:form of/functions]] contains functions for use with [[Module:form of/data/1]] and [[Module:form of/cats]].
They are contained in this module because data-only modules can't contain code. The functions in this file are of two
types:
*# Display handlers allow for customization of the display of multipart tags (see below). Currently there is only
one such handler, for handling multipart person tags such as `1//2//3`.
*# Cat functions allow for more complex categorization logic, and are referred to by name in [[Module:form of/cats]].
Currently no such functions exist.
The following terminology is used in conjunction with {{tl|inflection of}}:
* A ''tag'' is a single grammatical item, as specified in a single numbered parameter of {{tl|inflection of}}. Examples
are `masculine`, `nominative`, or `first-person`. Tags may be abbreviated, e.g. `m` for `masculine`, `nom` for
`nominative`, or `1` for `first-person`. Such abbreviations are called ''aliases'', and some tags have multiple
equivalent aliases (e.g. `p` or `pl` for `plural`). The full, non-abbreviated form of a tag is called its
''canonical form''.
* The ''display form'' of a tag is the way it's displayed to the user. Usually the displayed text of the tag is the same
as its canonical form, and it normally functions as a link to a glossary entry explaining the tag. Usually the link is
to an entry in [[Appendix:Glossary]], but sometimes the tag is linked to an individual dictionary entry or to a
Wikipedia entry. Occasionally, the display text differs from the canonical form of the tag. An example is the tag
`comparative case`, which has the display text read as simply `comparative`. Normally, tags referring to cases don't
have the word "case" in them, but in this case the tag `comparative` was already used as an alias for the tag
`comparative degree`, so the tag was named `comparative case` to avoid clashing. A similar situation occurs with
`adverbial case` vs. the grammar tag `adverbial` (as in `adverbial participle`).
* A ''tag set'' is an ordered list of tags, which together express a single inflection, for example, `1|s|pres|ind`,
which can be expanded to canonical-form tags as `first-person|singular|present|indicative`.
* A ''conjoined tag set'' is a tag set that consists of multiple individual tag sets separated by a semicolon, e.g.
`1|s|pres|ind|;|2|s|imp`, which specifies two tag sets, `1|s|pres|ind` as above and `2|s|imp` (in canonical form,
`second-person|singular|imperative`). Multiple tag sets specified in a single call to {{tl|inflection of}} are
specified in this fashion. Conjoined tag sets can also occur in list-tag shortcuts.
* A ''multipart tag'' is a tag that embeds multiple tags within it, such as `f//n` or `nom//acc//voc`. These are used in
the case of [[syncretism]], when the same form applies to multiple inflections. Examples are the Spanish present
subjunctive, where the first-person and third-person singular have the same form (e.g. {{m|es|siga}} from
{{m|es|seguir|t=to follow}}), or Latin third-declension adjectives, where the dative and ablative plural of all
genders have the same form (e.g. {{m|la|omnibus}} from {{m|la|omnis|t=all}}). These would be expressed respectively as
`1//3|s|pres|sub` and `dat//abl|m//f//n|p`, where the use of the multipart tag compactly encodes the syncretism and
avoids the need to individually list out all of the inflections. Multipart tags currently display as a list separated
by a slash, e.g. ''dative/ablative'' or ''masculine/feminine/neuter'' where each individual word is linked
appropriately. As a special case, multipart tags involving persons display specially; for example, the multipart tag
`1//2//3` displays as ''first-, second- and third-person'', with the word "person" occurring only once.
* A ''two-level multipart tag'' is a special type of multipart tag that joins two or more tag sets instead of joining
individual tags. The tags within the tag set are joined by a colon, e.g. `1:s//3:p`, which is displayed as
''first-person singular and third-person plural'', e.g. for use with the form {{m|grc|μέλλον}} of the verb
{{m|grc|μέλλω|t=to intend}}, which uses the tag set `1:s//3:p|impf|actv|indc|unaugmented` to express the syncretism
between the first singular and third plural forms of the imperfect active indicative unaugmented conjugation.
Two-level multipart tags should be used sparingly; if in doubt, list out the inflections separately. [FIXME: Make
two-level multipart tags obsolete.]
* A ''shortcut'' is a tag that expands to any type of tag described above, or to any type of tag set described above.
Aliases are a particular type of shortcut whose expansion is a single non-multipart tag.
* A ''multipart shortcut'' is a shortcut that expands into a multipart tag, for example `123`, which expands to the
multipart tag `1//2//3`. Only the most common such combinations exist as shortcuts.
* A ''list shortcut'' is a special type of shortcut that expands to a list of tags instead of a single tag. For example,
the shortcut `1s` expands to `1|s` (first-person singular). Only the most common such combinations exist as shortcuts.
* A ''conjoined shortcut'' is a special type of list shortcut that consists of a conjoined tag set (multiple logical tag
sets). For example, the English language-specific shortcut `ed-form` expands to `spast|;|past|part`, expressing the
common syncretism between simple past and past participle in English (and in this case, `spast` is itself a list
shortcut that expands to `simple|past`).]==]
-- Add tracking category for PAGE when called from {{inflection of}} or
-- similar TEMPLATE. The tracking category linked to is
-- [[Wiktionary:Tracking/inflection of/PAGE]].
local function track(page)
debug_track("inflection of/" ..
-- avoid including links in pages (may cause error)
page:gsub("%[", "("):gsub("%]", ")"):gsub("|", "!")
)
end
local function wrap_in_span(text, classes)
if classes then
return ("<span class='%s'>%s</span>"):format(classes, text)
else
return text
end
end
local function show_linked_term(data)
local termobj, face, span_classes, ok_to_destructively_modify, overall_lang, text_classes =
data.termobj, data.face, data.span_classes, data.ok_to_destructively_modify, data.overall_lang,
data.text_classes
local need_to_copy, pretext_lang
local categories = {}
if overall_lang and overall_lang:getCode() ~= termobj.lang:getCode() then
local lang_display
lang_display, categories = require(etymology_module).insert_source_cat_get_display {
lang = data.overall_lang,
source = termobj.lang,
}
pretext_lang = wrap_in_span(lang_display .. " ", text_classes)
end
local need_to_show_qualifiers = termobj.q or termobj.qq or termobj.a or termobj.aa or termobj.l or termobj.ll or
termobj.refs
need_to_copy = not ok_to_destructively_modify and (pretext_lang or need_to_show_qualifiers)
if need_to_copy then
termobj = shallow_copy(termobj)
end
if pretext_lang then
termobj.pretext = pretext_lang
end
if need_to_show_qualifiers then
termobj.show_qualifiers = true
end
return wrap_in_span(full_link(termobj, face), span_classes), categories
end
--[==[
Lowest-level implementation of form-of templates, including the general {{tl|form of}} as well as those that deal with
inflection tags, such as the general {{tl|inflection of}}, semi-specific variants such as {{tl|participle of}}, and
specific variants such as {{tl|past participle of}}. `data` contains all the information controlling the display, with
the following fields:
* `.lang`: Overall language of the form-of template. If specified, any lemmas, enclitics or base lemmas that are of a
different language will have that language displayed before the term in question.
* `.text`: Text to insert before the lemmas. Wrapped in the value of `.text_classes`, or its default; see below.
* `.lemmas`: List of objects describing the lemma(s) of which the term in question is a non-lemma form. These are passed
directly to {full_link()} in [[Module:links]]. Each object should have at minimum a `.lang` field containing the
language of the lemma and a `.term` field containing the lemma itself. Each object is formatted using {full_link()}
and then if there are more than one, they are joined using {serialCommaJoin()} in [[Module:table]]. Alternatively,
`.lemmas` can be a string, which is displayed directly, or omitted, to show no lemma links and omit the connecting
text.
* `.lemma_face`: "Face" to use when displaying the lemma objects. Usually should be set to {"term"}.
* `.conj`: Conjunction or separator to use when joining multiple lemma objects. If {nil}, defaults to {"and"}. If this
has the value {false}, the lemmas are preceded with the `.separator` field in each lemma.
* `.enclitics`: List of enclitics to display after the lemmas, in parens.
* `.enclitic_conj`: Conjunction or separator to use when joining multiple enclitics. Defaults to {"and"}.
* `.base_lemmas`: List of base lemmas to display after the lemmas, in the case where the lemmas in `.lemmas` are
themselves forms of another lemma (the base lemma), e.g. a comparative, superlative or participle. Each object is of
the form { { paramobj = PARAM_OBJ, lemmas = {LEMMA_OBJ, LEMMA_OBJ, ...}, conj = "CONJ" }} where PARAM_OBJ describes
the properties of the base lemma parameter (i.e. the relationship between the intermediate and base lemmas);
LEMMA_OBJ is an object suitable to be passed to {full_link()} in [[Module:links]]; and CONJ is the conjunction to
join multiple lemmas with, defaulting to {"and"}. PARAM_OBJ is of the format
{ { param = "PARAM", tags = {"TAG", "TAG", ...} } where PARAM is the name of the parameter to {{tl|inflection of}}
etc. that holds the base lemma(s) of the specified relationship and the tags describe the relationship, such as
{ {"comd"}} or { {"past", "part"}}.
* `.text_classes`: CSS classes used to wrap the tag text and lemma links. Default is
{"form-of-definition use-with-mention"}. Use `false` for no wrapping.
* `.lemma_classes`: Additional CSS classes used to wrap the lemma links. Default is {"form-of-definition-link"}.
Use `false` for no wrapping.
* `.posttext`: Additional text to display after the lemma links.
* `.ok_to_destructively_modify`: If set, data structures (including the nested lemma structures) can be modified
in-place to save memory; otherwise they will be copied before modifying.
Returns two values, the formatted string and any categories to add the page to (which will arise if `.lang` is
specified and a language other than `.lang` is given in one of the lemmas in `.lemmas` or enclitics in `.enclitics`).
]==]
function export.format_form_of(data)
if type(data) ~= "table" then
error("Internal error: First argument must now be a table of arguments")
end
local text_classes = data.text_classes
if text_classes == nil and not data.nowrap then
text_classes = "form-of-definition use-with-mention"
end
local lemma_classes = data.lemma_classes
if lemma_classes == nil then
lemma_classes = "form-of-definition-link"
end
local parts = {}
if text_classes then
insert(parts, "<span class='" .. text_classes .. "'>")
end
insert(parts, data.text)
if data.text ~= "" and data.lemmas then
insert(parts, " ")
end
local categories = {}
if data.lemmas then
if type(data.lemmas) == "string" then
insert(parts, wrap_in_span(data.lemmas, lemma_classes))
else
local formatted_terms = {}
for _, lemma in ipairs(data.lemmas) do
local linked_term, this_categories = show_linked_term {
termobj = lemma,
face = data.lemma_face,
span_classes = lemma_classes,
ok_to_destructively_modify = data.ok_to_destructively_modify,
overall_lang = data.lang,
text_classes = text_classes
}
if this_categories[1] then
extend(categories, this_categories)
end
if data.conj == false and lemma.separator then
insert(formatted_terms, lemma.separator)
end
insert(formatted_terms, linked_term)
end
if data.conj == false then
insert(parts, concat(formatted_terms))
else
insert(parts, serial_comma_join(formatted_terms, {conj = data.conj or "and"}))
end
end
if data.lit then
insert(parts, ", literally " .. require(links_module).mark(data.lit, "gloss"))
end
end
if data.enclitics and #data.enclitics > 0 then
-- The outer parens need to be outside of the text_classes span so they show in upright instead of italic, or
-- they will clash with upright parens generated by link annotations such as transliterations and pos=.
if text_classes then
insert(parts, "</span>")
end
local formatted_terms = {}
for _, enclitic in ipairs(data.enclitics) do
-- FIXME, should we have separate clitic face and/or classes?
local linked_term, this_categories = show_linked_term {
termobj = enclitic,
face = data.lemma_face,
span_classes = lemma_classes,
ok_to_destructively_modify = data.ok_to_destructively_modify,
overall_lang = data.lang,
text_classes = text_classes
}
if this_categories[1] then
extend(categories, this_categories)
end
insert(formatted_terms, linked_term)
end
insert(parts, " (")
insert(parts, wrap_in_span("with enclitic" .. (#data.enclitics > 1 and "s" or "") .. " ", text_classes))
insert(parts, serial_comma_join(formatted_terms, {conj = data.enclitic_conj or "and"}))
insert(parts, ")")
if text_classes then
insert(parts, "<span class='" .. text_classes .. "'>")
end
end
if data.base_lemmas and #data.base_lemmas > 0 then
for _, base_lemma in ipairs(data.base_lemmas) do
insert(parts, ", the ")
if text_classes then
insert(parts, "</span>")
end
insert(parts, (export.tagged_inflections {
lang = data.lang or base_lemma.lemmas[1].lang,
tags = base_lemma.paramobj.tags,
lemmas = base_lemma.lemmas,
conj = base_lemma.conj or "and",
lemma_face = data.lemma_face,
no_format_categories = true,
nocat = true,
text_classes = data.text_classes,
ok_to_destructively_modify = ok_to_destructively_modify,
}))
if text_classes then
insert(parts, "<span class='" .. text_classes .. "'>")
end
end
end
-- FIXME, should posttext go before enclitics? If so we need to have separate handling for the
-- final colon when there are multiple tag sets in tagged_inflections().
if data.posttext then
insert(parts, data.posttext)
end
if text_classes then
insert(parts, "</span>")
end
return concat(parts), categories
end
format_form_of = export.format_form_of
--[==[
Return true if `tag` contains an internal link or HTML.]==]
function export.is_link_or_html(tag)
return tag:find("[[", nil, true) or tag:find("|", nil, true) or tag:find("<", nil, true)
end
is_link_or_html = export.is_link_or_html
--[==[
Look up a tag (either a shortcut of any sort of a canonical long-form tag) and return its expansion. The expansion
will be a string unless the shortcut is a list-tag shortcut such as `1s`; in that case, the expansion will be a
list. The caller must handle both cases. Only one level of expansion happens; hence, `acc` expands to {"accusative"},
`1s` expands to { {"1", "s"}} (not to { {"first", "singular"}}) and `123` expands to {"1//2//3"}. The expansion will be
the same as the passed-in tag in the following circumstances:
# The tag is `;` (this is special-cased, and no lookup is done).
# The tag is a multipart tag such as `nom//acc` (this is special-cased, and no lookup is done).
# The tag contains a raw link (this is special-cased, and no lookup is done).
# The tag contains HTML (this is special-cased, and no lookup is done).
# The tag is already a canonical long-form tag.
# The tag is unrecognized.
This function first looks up in the lang-specific data module [[Module:form of/lang-data/LANGCODE]], then in
[[Module:form of/data/1]] (which includes more common non-lang-specific tags) and finally (only if the tag is not
recognized as a shortcut or canonical tag, and is not of types 1-4 above) in [[Module:form of/data/2]].
If the expansion is a string and is different from the tag, track it if `do_track` is true.]==]
function export.lookup_shortcut(tag, lang, do_track)
-- If there is HTML or a link in the tag, return it directly; don't try
-- to look it up, which will fail.
if tag == ";" or tag:find("//", nil, true) or is_link_or_html(tag) then
return tag
end
local expansion
while lang do
local langdata = safe_load_data(form_of_lang_data_module_prefix .. lang:getCode())
-- If this is a canonical long-form tag, just return it, and don't check for shortcuts. This is an
-- optimization; see below.
if langdata then
if langdata.tags[tag] then
return tag
end
expansion = langdata.shortcuts[tag]
if expansion then
break
end
end
-- If the language has a parent (i.e. a superordinate variety), try again with that.
lang = lang:getParent()
end
if not expansion then
-- If this is a canonical long-form tag, just return it, and don't check for shortcuts (which will cause
-- [[Module:form of/data/2]] to be loaded, because there won't be a shortcut entry in [[Module:form of/data/1]] --
-- or, for that matter, in [[Module:form of/data/2]]). This is an optimization; the code will still work without
-- it, but will use up more memory.
if (m_data1 or get_m_data1()).tags[tag] then
return tag
end
expansion = m_data1.shortcuts[tag]
end
if not expansion then
expansion = (m_data2 or get_m_data2()).shortcuts[tag]
end
if not expansion then
return tag
end
-- Maybe track the expansion if it's not the same as the raw tag.
if do_track and expansion ~= tag and type(expansion) == "string" then
track("tag/" .. tag)
end
return expansion
end
lookup_shortcut = export.lookup_shortcut
--[==[
Look up a normalized/canonicalized tag and return the data object associated with it. If the tag isn't found, return
nil. This first looks up in the lang-specific data module [[Module:form of/lang-data/LANGCODE]], then in
[[Module:form of/data/1]] (which includes more common non-lang-specific tags) and then finally in
[[Module:form of/data/2]].]==]
function export.lookup_tag(tag, lang)
while lang do
local langdata = safe_load_data(form_of_lang_data_module_prefix .. lang:getCode())
local tag = langdata and langdata.tags[tag]
if tag then
return tag
end
-- If the language has a parent (i.e. a superordinate variety), try again with that.
lang = lang:getParent()
end
local tagobj = (m_data1 or get_m_data1()).tags[tag]
if tagobj then
return tagobj
end
local tagobj2 = (m_data2 or get_m_data2()).tags[tag]
if tagobj2 then
return tagobj2
end
return nil
end
lookup_tag = export.lookup_tag
-- Normalize a single tag, which may be a shortcut but should not be a multipart tag, a multipart shortcut or a list
-- shortcut.
local function normalize_single_tag(tag, lang, do_track)
local expansion = lookup_shortcut(tag, lang, do_track)
if type(expansion) ~= "string" then
error("Tag '" .. tag .. "' is a list shortcut, which is not allowed here")
end
tag = expansion
if not lookup_tag(tag, lang) and do_track then
-- If after all expansions and normalizations we don't recognize the canonical tag, track it.
track("unknown")
track("unknown/" .. tag)
end
return tag
end
--[=[
Normalize a component of a multipart tag. This should not have any // in it, but may join multiple individual tags with
a colon, and may be a single list-tag shortcut, which is treated as if colon-separated. The return value may be a list
of tags.
]=]
local function normalize_multipart_component(tag, lang, do_track)
-- If there is HTML or a link in the tag, don't try to split on colon. A colon may legitimately occur in either one,
-- and we don't want these things parsed. Note that we don't do this check before splitting on //, which we don't
-- expect to occur in links or HTML; see comment in normalize_tag().
if is_link_or_html(tag) then
return tag
end
local components = split(tag, ":", true)
if #components == 1 then
-- We allow list-tag shortcuts inside of multipart tags, e.g.
-- '1s//3p'. Check for this now.
tag = lookup_shortcut(tag, lang, do_track)
if type(tag) == "table" then
-- Temporary tracking as we will disallow this.
track("list-tag-inside-of-multipart")
-- We found a list-tag shortcut; treat as if colon-separated.
components = tag
else
return normalize_single_tag(tag, lang, do_track)
end
end
local normtags = {}
-- Temporary tracking as we will disallow this.
track("two-level-multipart")
for _, component in ipairs(components) do
if do_track then
-- There are multiple components; track each of the individual
-- raw tags.
track("tag/" .. component)
end
insert(normtags, normalize_single_tag(component, lang, do_track))
end
return normtags
end
--[=[
Normalize a single tag. The return value may be a list (in the case of multipart tags), which will contain nested lists
in the case of two-level multipart tags.
]=]
local function normalize_tag(tag, lang, do_track)
-- We don't check for links or HTML before splitting on //, which we don't expect to occur in links or HTML. Doing
-- it this way allows for a tag like '{{lb|grc|Epic}}//{{lb|grc|Ionic}}' to function correctly (the template calls
-- will be expanded before we process the tag, and will contain links and HTML). The only check we do is for a URL,
-- which shouldn't normally occur, but might if the user tries to put an external link into the tag. URL's with //
-- normally have the sequence ://, which should never normally occur when // and : are used in their normal ways.
if tag:find("://", nil, true) then
return tag
end
local split_tags = split(tag, "//", true)
if #split_tags == 1 then
local retval = normalize_multipart_component(tag, lang, do_track)
if type(retval) == "table" then
-- The user gave a tag like '1:s', i.e. with colon but without //. Allow this, but we need to return a
-- nested list.
return {retval}
end
return retval
end
local normtags = {}
for _, single_tag in ipairs(split_tags) do
if do_track then
-- If the tag was a multipart tag, track each of individual raw tags.
track("tag/" .. single_tag)
end
insert(normtags, normalize_multipart_component(single_tag, lang, do_track))
end
return normtags
end
--[==[
Normalize a tag set (a list of tags) into its canonical-form tags. The return value is a list of normalized tag sets
(a list because of there may be conjoined shortcuts among the input tags). A normalized tag set is a list of tag
elements, where each element is either a string (the canonical form of a tag), a list of such strings (in the case of
multipart tags) or a list of lists of such strings (in the case of two-level multipart tags). For example, the multipart
tag `nom//acc//voc` will be represented in canonical form as { {"nominative", "accusative", "vocative"}}, and the
two-level multipart tag `1:s//3:p` will be represented as { {{"first-person", "singular"}, {"third-person", "plural"}}}.
Example 1:
{normalize_tag_set({"nom//acc//voc", "n", "p"})} = { {{{"nominative", "accusative", "vocative"}, "masculine", "plural"}}}
Example 2:
{normalize_tag_set({"ed-form"}, ENGLISH)} = { {{"simple", "past"}, {"past", "participle"}}}
Example 3:
{normalize_tag_set({"archaic", "ed-form"}, ENGLISH)} = { {{"archaic", "simple", "past"}, {"archaic", "past", "participle"}}}]==]
function export.normalize_tag_set(tag_set, lang, do_track)
-- We track usage of shortcuts, normalized forms and (in the case of multipart tags or list tags) intermediate
-- forms. For example, if the tags 1s|mn|gen|indefinite are passed in, we track the following:
-- [[Wiktionary:Tracking/inflection of/tag/1s]]
-- [[Wiktionary:Tracking/inflection of/tag/1]]
-- [[Wiktionary:Tracking/inflection of/tag/s]]
-- [[Wiktionary:Tracking/inflection of/tag/first-person]]
-- [[Wiktionary:Tracking/inflection of/tag/singular]]
-- [[Wiktionary:Tracking/inflection of/tag/mn]]
-- [[Wiktionary:Tracking/inflection of/tag/m//n]]
-- [[Wiktionary:Tracking/inflection of/tag/m]]
-- [[Wiktionary:Tracking/inflection of/tag/n]]
-- [[Wiktionary:Tracking/inflection of/tag/masculine]]
-- [[Wiktionary:Tracking/inflection of/tag/neuter]]
-- [[Wiktionary:Tracking/inflection of/tag/gen]]
-- [[Wiktionary:Tracking/inflection of/tag/genitive]]
-- [[Wiktionary:Tracking/inflection of/tag/indefinite]]
local output_tag_set = {}
local saw_semicolon = false
for _, tag in ipairs(tag_set) do
if do_track then
-- Track the raw tag.
track("tag/" .. tag)
end
-- Expand the tag, which may generate a new tag (either a fully canonicalized tag, a multipart tag, or a list
-- of tags).
tag = lookup_shortcut(tag, lang, do_track)
if type(tag) == "table" then
if contains(tag, ";") then
-- If we saw a conjoined shortcut, we need to use a more general algorithm that can expand a single
-- tag set into multiple.
saw_semicolon = true
break
end
for _, t in ipairs(tag) do
if do_track then
-- If the tag expands to a list of raw tags, track each of those.
track("tag/" .. t)
end
insert(output_tag_set, normalize_tag(t, lang, do_track))
end
else
insert(output_tag_set, normalize_tag(tag, lang, do_track))
end
end
if not saw_semicolon then
return {output_tag_set}
end
-- Use a more general algorithm that handles conjoined shortcuts.
output_tag_set = {}
for i, tag in ipairs(tag_set) do
if do_track then
-- Track the raw tag.
track("tag/" .. tag)
end
-- Expand the tag, which may generate a new tag (either a fully canonicalized tag, a multipart tag, or a list
-- of tags).
tag = lookup_shortcut(tag, lang, do_track)
if type(tag) == "table" then
local output_tag_sets = {}
local shortcut_tag_sets = split_tag_set(tag)
local normalized_shortcut_tag_sets = {}
for _, shortcut_tag_set in ipairs(shortcut_tag_sets) do
extend(normalized_shortcut_tag_sets,
normalize_tag_set(shortcut_tag_set, lang, do_track))
end
local after_tags = slice(tag_set, i + 1)
local normalized_after_tags_sets = normalize_tag_set(after_tags, lang, do_track)
for _, normalized_shortcut_tag_set in ipairs(normalized_shortcut_tag_sets) do
for _, normalized_after_tags_set in ipairs(normalized_after_tags_sets) do
insert(output_tag_sets, append(output_tag_set, normalized_shortcut_tag_set,
normalized_after_tags_set))
end
end
return output_tag_sets
else
insert(output_tag_set, normalize_tag(tag, lang, do_track))
end
end
error("Internal error: Should not get here")
end
normalize_tag_set = export.normalize_tag_set
--[==[
Split a tag set that may consist of multiple semicolon-separated tag sets into the component tag sets.]==]
function export.split_tag_set(tag_set)
local split_tag_sets = {}
local cur_tag_set = {}
for _, tag in ipairs(tag_set) do
if tag == ";" then
if #cur_tag_set > 0 then
insert(split_tag_sets, cur_tag_set)
end
cur_tag_set = {}
else
insert(cur_tag_set, tag)
end
end
if #cur_tag_set > 0 then
insert(split_tag_sets, cur_tag_set)
end
return split_tag_sets
end
split_tag_set = export.split_tag_set
local tag_set_param_mods = {
lb = {
item_dest = "labels",
convert = function(arg, parse_err)
return split(arg, "//", true)
end,
}
}
--[==[
Parse tag set properties from a tag set (list of tags). Currently no per-tag properties are recognized, and the only
per-tag-set property recognized is `<lb:...>` for specifing label(s) for the tag set. Per-tag-set properties must be
attached to the last tag.]==]
function export.parse_tag_set_properties(tag_set)
local function generate_tag_set_obj(last_tag)
tag_set[#tag_set] = last_tag
return {tags = tag_set}
end
local last_tag = tag_set[#tag_set]
-- Check for inline modifier, e.g. מרים<tr:Miryem>. But exclude HTML entry with <span ...>, <i ...>, <br/> or
-- similar in it, caused by wrapping an argument in {{l|...}}, {{af|...}} or similar. Basically, all tags of
-- the sort we parse here should consist of a less-than sign, plus letters, plus a colon, e.g. <lb:...>, so if
-- we see a tag on the outer level that isn't in this format, we don't try to parse it. The restriction to the
-- outer level is to allow generated HTML inside of e.g. qualifier tags, such as foo<q:similar to {{m|fr|bar}}>.
if last_tag:find("<", nil, true) and not last_tag:find("^[^<]*<%l*[^%l:]") then
return parse_inline_modifiers(last_tag, {
param_mods = tag_set_param_mods,
generate_obj = generate_tag_set_obj,
})
else
return generate_tag_set_obj(last_tag)
end
end
parse_tag_set_properties = export.parse_tag_set_properties
local function normalize_pos(pos)
if not pos then
return nil
end
return (m_pos_data or get_m_pos_data())[pos] or pos
end
-- Return the display form of a single canonical-form tag. The value
-- passed in must be a string (i.e. it cannot be a list describing a
-- multipart tag). To handle multipart tags, use get_tag_display_form().
-- A truthy `nolink` suppresses linking.
local function get_single_tag_display_form(normtag, lang, nolink)
local data = lookup_tag(normtag, lang)
local display = normtag
-- If the tag has a special display form, use it
if data and data.display then
display = data.display
if nolink then
display = remove_links(display)
end
end
-- If there is a nonempty glossary index, then show a link to it
if not nolink then
local glossary = data and data[(m_data or get_m_data()).GLOSSARY]
if glossary ~= nil then
if glossary == m_data.WIKT then
display = "[[" .. normtag .. "|" .. display .. "]]"
elseif glossary == m_data.WP then
display = "[[w:" .. normtag .. "|" .. display .. "]]"
elseif glossary == m_data.APPENDIX then
display = "[[Appendix:Glossary#" .. anchor_encode(normtag) .. "|" .. display .. "]]"
elseif type(glossary) ~= "string" then
error(("Internal error: Wrong type %s for glossary value %s for tag %s"):format(
type(glossary), dump(glossary), normtag))
else
local link = glossary:match("^wikt:(.*)")
if link then
display = "[[" .. link .. "|" .. display .. "]]"
end
if not link then
link = glossary:match("^w:(.*)")
if link then
display = "[[w:" .. link .. "|" .. display .. "]]"
end
end
if not link then
display = "[[Appendix:Glossary#" .. anchor_encode(glossary) .. "|" .. display .. "]]"
end
end
end
end
return display
end
--[==[
Turn a canonicalized tag spec (which describes a single, possibly multipart tag) into the displayed form. The tag spec
may be a string (a canonical-form tag); a list of canonical-form tags (in the case of a simple multipart tag); or a
list of mixed canonical-form tags and lists of such tags (in the case of a two-level multipart tag). `joiner` indicates
how to join the parts of a multipart tag, and can be either {"and"} ("foo and bar", or "foo, bar and baz" for 3 or
more), {"slash"} ("foo/bar"), {"en-dash"} ("foo–bar") or {nil}, which uses the global default found in
{multipart_join_strategy()} in [[Module:form of/functions]]. (NOTE: The global default is {"slash"} and this seems
unlikely to change.) A truthy `nolink` suppresses linking.]==]
function export.get_tag_display_form(tagspec, lang, joiner, nolink)
if type(tagspec) == "string" then
return get_single_tag_display_form(tagspec, lang, nolink)
end
-- We have a multipart tag. See if there's a display handler to display them specially.
for _, handler in ipairs(display_handlers or get_display_handlers()) do
local displayval = handler(tagspec, joiner)
if displayval then
if nolink then
displayval = remove_links(displayval)
end
return displayval
end
end
-- No display handler.
local displayed_tags = {}
for _, first_level_tag in ipairs(tagspec) do
if type(first_level_tag) == "string" then
insert(displayed_tags, get_single_tag_display_form(first_level_tag, lang, nolink))
else
-- A first-level element of a two-level multipart tag. Currently we just separate the individual components
-- with spaces, but other ways are possible, e.g. using an underscore, colon, parens or braces.
local components = {}
for _, component in ipairs(first_level_tag) do
insert(components, get_single_tag_display_form(component, lang, nolink))
end
insert(displayed_tags, concat(components, " "))
end
end
return join_multiparts(displayed_tags, joiner)
end
get_tag_display_form = export.get_tag_display_form
--[==[
Given a normalized tag set (i.e. as output by {normalize_tag_set()}; all tags are in canonical form, multipart tags are
represented as lists, and two-level multipart tags as lists of lists), convert to displayed form (a string). See
{get_tag_display_form()} for the meaning of `joiner`. A truthy `nolink` suppresses linking.]==]
function export.get_tag_set_display_form(normalized_tag_set, lang, joiner, nolink)
local parts = {}
for _, tagspec in ipairs(normalized_tag_set) do
local to_insert = get_tag_display_form(tagspec, lang, joiner, nolink)
-- Maybe insert a space before inserting the display form of the tag. We insert a space if
-- (a) we're not the first tag; and
-- (b) the tag we're about to insert doesn't have the "no_space_on_left" property; and
-- (c) the preceding tag doesn't have the "no_space_on_right" property.
-- NOTE: We depend here on the fact that
-- (1) all tags with either of the above properties set have the same display form as canonical form, and
-- (2) all tags with either of the above properties set are single-character tags.
-- The second property is an optimization to avoid looking up display forms resulting from multipart tags,
-- which won't be found and which will trigger loading of [[Module:form of/data/2]]. If multichar punctuation is
-- added in the future, it's ok to change the == 1 below to <= 2 or <= 3.
--
-- If the first property above fails to hold in the future, we need to track the canonical form of each tag
-- (including the previous one) as well as the display form. This would also avoid the need for the == 1 check.
if #parts > 0 then
local most_recent_tagobj = parts[#parts]:match("^.[\128-\191]*$") and lookup_tag(parts[#parts], lang)
local to_insert_tagobj = to_insert:match("^.[\128-\191]*$") and lookup_tag(to_insert, lang)
if (
(not most_recent_tagobj or not most_recent_tagobj.no_space_on_right) and
(not to_insert_tagobj or not to_insert_tagobj.no_space_on_left)
) then
insert(parts, " ")
end
end
insert(parts, to_insert)
end
return concat(parts)
end
get_tag_set_display_form = export.get_tag_set_display_form
--[==[
Split a tag set containing two-level multipart tags into one or more tag sets not containing such tags.
Single-level multipart tags are left alone. (If we need to, a slight modification of the following code
will also split single-level multipart tags.) This assumes that multipart tags are represented as lists
and two-level multipart tags are represented as lists of lists, as is output by {normalize_tag_set()}.
NOTE: We have to be careful to properly handle imbalanced two-level multipart tags such as
`def:s//p` (or the reverse, `s//def:p`).]==]
local function split_two_level_multipart_tag_set(tag_set)
for i, tag in ipairs(tag_set) do
if type(tag) == "table" then
-- We saw a multipart tag. Check if any of the parts are two-level.
local saw_two_level_tag = false
for _, first_level_tag in ipairs(tag) do
if type(first_level_tag) == "table" then
saw_two_level_tag = true
break
end
end
if saw_two_level_tag then
-- We found a two-level multipart tag.
-- (1) Extract the preceding tags.
local pre_tags = slice(tag_set, 1, i - 1)
-- (2) Extract the following tags.
local post_tags = slice(tag_set, i + 1)
-- (3) Loop over each tag set alternant in the two-level multipart tag.
-- For each alternant, form the tag set consisting of pre_tags + alternant + post_tags,
-- and recursively split that tag set.
local resulting_tag_sets = {}
for _, first_level_tag_set in ipairs(tag) do
local expanded_tag_set = {}
extend(expanded_tag_set, pre_tags)
-- The second level may have a string or a list.
if type(first_level_tag_set) == "table" then
extend(expanded_tag_set, first_level_tag_set)
else
insert(expanded_tag_set, first_level_tag_set)
end
extend(expanded_tag_set, post_tags)
extend(resulting_tag_sets, split_two_level_multipart_tag_set(expanded_tag_set))
end
return resulting_tag_sets
end
end
end
return {tag_set}
end
local function try_lang_specific_module(langcode, modules_tried, name, data)
local lang_specific_module = form_of_lang_data_module_prefix .. langcode .. "/functions"
local langdata = safe_require(lang_specific_module)
if langdata then
insert(modules_tried, lang_specific_module)
if langdata.cat_functions then
local fn = langdata.cat_functions[name]
if fn then
return fn(data), true
end
end
end
return nil, false
end
-- Call a named function, either from the lang-specific data in
-- [[Module:form of/lang-specific/LANGCODE/functions]] or in [[Module:form of/functions]].
local function call_named_function(name, funtype, normalized_tag_set, lang, POS, pagename, lemmas)
local data = {
pagename = pagename or default_pagename or get_default_pagename(),
lemmas = lemmas,
tag_set = normalized_tag_set,
lang = lang,
POS = POS
}
local modules_tried = {}
-- First try lang-specific.
while lang do
local retval, found_it = try_lang_specific_module(lang:getCode(), modules_tried, name, data)
if found_it then
return retval
end
-- If the language has a parent (i.e. a superordinate variety), try again with that.
lang = lang:getParent()
end
-- Try lang-independent.
insert(modules_tried, form_of_functions_module)
local fn = (cat_functions or get_cat_functions())[name]
if fn then
return fn(data)
end
for i, modname in ipairs(modules_tried) do
modules_tried[i] = "[[" .. modname .. "]]"
end
error(("No %s function named '%s' in %s"):format(funtype, name, list_to_text(modules_tried, nil, " or ")))
end
-- Given a tag from the current tag set (which may be a list in case of a multipart tag),
-- and a tag from a categorization spec, check that the two match.
-- (1) If both are strings, we just check for equality.
-- (2) If the spec tag is a string and the tag set tag is a list (i.e. it originates from a
-- multipart tag), we check that the spec tag is in the list. This is because we want to treat
-- multipart tags in user-specified tag sets as if the user had specified multiple tag sets.
-- For example, if the user said "1//3|s|pres|ind" and the categorization spec says {"has", "1"},
-- we want this to match, because "1//3|s|pres|ind" should be treated equivalently to two tag
-- sets "1|s|pres|ind" and "3|s|pres|ind", and the former matches the categorization spec.
-- (3) If the spec tag is a list (i.e. it originates from a multipart tag), we check that the
-- tag set tag is also a list and is a superset of the spec tag. For example, if the categorization
-- spec says {"has", "1//3"}, then the tag set tag must be a multipart tag that has both "1" and "3"
-- in it. "1//3" works, as does "1//2//3".
local function tag_set_tag_matches_spec_tag(tag_set_tag, spec_tag)
if type(spec_tag) == "table" then
if type(tag_set_tag) == "table" and is_subset_list(spec_tag, tag_set_tag) then
return true
end
elseif type(tag_set_tag) == "table" then
if contains(tag_set_tag, spec_tag) then
return true
end
elseif tag_set_tag == spec_tag then
return true
end
return false
end
-- Check that the current tag set matches the given spec tag. This means that any of the tags
-- in the current tag set match, according to tag_set_tag_matches_spec_tag(); see above. If the
-- current tag set contains only string tags (i.e. no multipart tags), and the spec tag is a
-- string (i.e. not a multipart tag), this boils down to list containment, but it gets more
-- complex when multipart tags are present.
local function tag_set_matches_spec_tag(spec_tag, tag_set, lang)
spec_tag = normalize_tag(spec_tag, lang)
for _, tag_set_tag in ipairs(tag_set) do
if tag_set_tag_matches_spec_tag(tag_set_tag, spec_tag) then
return true
end
end
return false
end
-- Check whether the given spec matches the current tag set. Two values are returned:
-- (1) whether the spec matches the tag set; (2) the index of the category to add if
-- the spec matches.
local function check_condition(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
if type(spec) == "boolean" then
return spec
elseif type(spec) ~= "table" then
error("Wrong type of condition " .. spec .. ": " .. type(spec))
end
local predicate = spec[1]
if predicate == "has" then
return tag_set_matches_spec_tag(spec[2], tag_set, lang), 3
elseif predicate == "hasall" then
for _, tag in ipairs(spec[2]) do
if not tag_set_matches_spec_tag(tag, tag_set, lang) then
return false, 3
end
end
return true, 3
elseif predicate == "hasany" then
for _, tag in ipairs(spec[2]) do
if tag_set_matches_spec_tag(tag, tag_set, lang) then
return true, 3
end
end
return false, 3
elseif predicate == "tags=" then
local normalized_spec_tag_sets = normalize_tag_set(spec[2], lang)
if #normalized_spec_tag_sets > 1 then
error("Internal error: No support for conjoined shortcuts in category/label specs in "
.. "[[Module:form of/cats]] when processing spec tag set " .. concat(spec[2], "|"))
end
local normalized_spec_tag_set = normalized_spec_tag_sets[1]
-- Check for and disallow two-level multipart tags in the specs. FIXME: Remove this when we remove
-- support for two-level multipart tags.
for _, tag in ipairs(normalized_spec_tag_set) do
if type(tag) == "table" then
for _, subtag in ipairs(tag) do
if type(subtag) == "table" then
error("Internal error: No support for two-level multipart tags in category/label specs"
.. "[[Module:form of/cats]] when processing spec tag set "
.. concat(spec[2], "|"))
end
end
end
end
-- Allow tags to be in different orders, and multipart tags to be in different orders. To handle this,
-- we first check that both tag set tags and spec tags have the same length. If so, we sort the
-- multipart tags in the tag set tags and spec tags, and then check that all tags in the spec tags are
-- in the tag set tags.
if #tag_set ~= #normalized_spec_tag_set then
return false, 3
end
local tag_set_tags = deep_copy(tag_set)
for i=1,#tag_set_tags do
if type(tag_set_tags[i]) == "table" then
sort(tag_set_tags[i])
end
if type(normalized_spec_tag_set[i]) == "table" then
sort(normalized_spec_tag_set[i])
end
end
for i=1,#tag_set_tags do
if not contains(tag_set_tags, normalized_spec_tag_set[i]) then
return false, 3
end
end
return true, 3
elseif predicate == "p=" then
return POS == normalize_pos(spec[2]), 3
elseif predicate == "pany" then
for _, specpos in ipairs(spec[2]) do
if POS == normalize_pos(specpos) then
return true, 3
end
end
return false, 3
elseif predicate == "pexists" then
return POS ~= nil, 2
elseif predicate == "not" then
local condval = check_condition(spec[2], tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
return not condval, 3
elseif predicate == "and" then
local condval = check_condition(spec[2], tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
if condval then
condval = check_condition(spec[3], tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
end
return condval, 4
elseif predicate == "or" then
local condval = check_condition(spec[2], tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
if not condval then
condval = check_condition(spec[3], tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
end
return condval, 4
elseif predicate == "call" then
return call_named_function(spec[2], "condition", normalized_tag_set, lang, POS, pagename, lemmas), 3
else
error("Unrecognized predicate: " .. predicate)
end
end
-- Process a given spec. This checks any conditions in the spec against the
-- tag set, and insert any resulting categories into `categories`. Return value
-- is true if the outermost condition evaluated to true and a category was inserted
-- (this is used in {"cond" ...} conditions, which stop when a subcondition evaluates
-- to true).
local function process_spec(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
if not spec then
return false
elseif type(spec) == "string" then
-- A category. Substitute POS request with user-specified part of speech or default.
spec = spec:gsub("<<p=(.-)>>", function(default)
return POS or normalize_pos(default)
end)
insert(categories, lang:getFullName() .. " " .. spec)
return true
elseif type(spec) == "table" and spec.labels then
-- A label spec.
for _, label in ipairs(spec.labels) do
insert_if_not(labels, label)
end
return true
elseif type(spec) ~= "table" then
error("Wrong type of specification " .. spec .. ": " .. type(spec))
end
local predicate = spec[1]
if predicate == "multi" then
for _, sp in iterate_from(2, ipairs(spec)) do -- Iterate from 2.
process_spec(sp, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
end
return true
elseif predicate == "cond" then
for _, sp in iterate_from(2, ipairs(spec)) do -- Iterate from 2.
if process_spec(sp, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels) then
return true
end
end
return false
elseif predicate == "call" then
return process_spec(
call_named_function(spec[2], "spec", normalized_tag_set, lang, POS, pagename, lemmas),
tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels
)
else
local condval, ifspec = check_condition(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas)
if condval then
process_spec(spec[ifspec], tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
return true
else
process_spec(spec[ifspec + 1], tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
-- FIXME: Are we sure this is correct?
return false
end
end
end
--[==[
Given a normalized tag set (i.e. as output by {normalize_tag_set()}; all tags are in canonical form, multipart tags are
represented as lists, and two-level multipart tags as lists of lists), fetch the associated categories and labels.
Return two values, a list of categories and a list of labels. `lang` is the language of term represented by the tag set,
and `POS` is the user-provided part of speech (which may be {nil}).]==]
function export.fetch_categories_and_labels(normalized_tag_set, lang, POS, pagename, lemmas)
local categories, labels = {}, {}
POS = normalize_pos(POS)
-- First split any two-level multipart tags into multiple sets, to make our life easier.
for _, tag_set in ipairs(split_two_level_multipart_tag_set(normalized_tag_set)) do
local langcode = lang:getCode()
local langspecs = (m_cats_data or get_m_cats_data())[langcode]
if langspecs then
for _, spec in ipairs(langspecs) do
process_spec(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
end
end
local full_code = lang:getFullCode()
if full_code ~= langcode then
local langspecs = (m_cats_data or get_m_cats_data())[full_code]
if langspecs then
for _, spec in ipairs(langspecs) do
process_spec(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
end
end
end
if full_code ~= "und" then
local langspecs = (m_cats_data or get_m_cats_data())["und"]
if langspecs then
for _, spec in ipairs(langspecs) do
process_spec(spec, tag_set, normalized_tag_set, lang, POS, pagename, lemmas, categories, labels)
end
end
end
end
return categories, labels
end
fetch_categories_and_labels = export.fetch_categories_and_labels
local function format_labels(labels, data, notext)
if labels and #labels > 0 then
return show_labels{
labels = labels,
lang = data.lang,
sort = data.sort,
nocat = data.nocat
} .. (notext and (data.pretext or "") == "" and "" or " ")
else
return ""
end
end
--[==[
Implementation of templates that display inflection tags, such as the general {{tl|inflection of}}, semi-specific
variants such as {{tl|participle of}}, and specific variants such as {{tl|past participle of}}. `data` contains all the
information controlling the display, with the following fields:
* `.lang`: ('''''required''''') Language to use when looking up language-specific inflection tags, categories and
labels, and for displaying categories and labels.
* `.tags`: ('''''required''' unless `.tag_sets` is given'') List of non-canonicalized inflection tags. Multiple tag sets
can be indicated by a {";"} as one of the tags, and tag-set properties may be attached to the last tag of a tag set.
The tags themselves may come directly from the user (as in {{tl|inflection of}}); come partly from the user (as in
{{tl|participle of}}, which adds the tag `part` to user-specified inflection tags); or be entirely specified by the
template (as in {{tl|past participle of}}).
* `.tag_sets`: ('''''required''' unless `.tags` is given'') List of non-canonicalized tag sets and associated
per-tag-set properties. Each element of the list is an object of the form
{ {tags = {"TAG", "TAG", ...}, labels = {"LABEL", "LABEL", ...}}. If `.tag_sets` is specified, `.tags` should not be
given and vice-versa. Specifying `.tag_sets` in place of tags allowed per-tag set labels to be specified; otherwise,
there is no advantage. [[Module:pt-gl-inflections]] uses this functionality to supply labels like {"Brazil"} and
{"Portugal"} associated with specific tag sets.
* `.lemmas`: ('''''recommended''''') List of objects describing the lemma(s) of which the term in question is a
non-lemma form. These are passed directly to {full_link()} in [[Module:links]]. Each object should have at minimum a
`.lang` field containing the language of the lemma and a `.term` field containing the lemma itself. Each object is
formatted using {full_link()} and then if there are more than one, they are joined using {serialCommaJoin()} in
[[Module:table]]. Alternatively, `.lemmas` can be a string, which is displayed directly. If omitted entirely, no lemma
links are shown and the connecting "of" is also omitted.
* `.lemma_face`: ('''''recommended''''') "Face" to use when displaying the lemma objects. Usually should be set to
{"term"}.
* `.POS`: ('''''recommended''''') Categorizing part-of-speech tag. Comes from the {{para|p}} or {{para|POS}} argument of
{{tl|inflection of}}.
* `.pagename`: Page name of "current" page or nil to use the actual page title; for testing purposes.
* `.conj`: Conjunction or separator to use when joining multiple lemma objects. Defaults to {"and"}.
* `.enclitics`: List of enclitics to display after the lemmas, in parens.
* `.enclitic_conj`: Conjunction or separator to use when joining multiple enclitics. Defaults to {"and"}.
* `.no_format_categories`: If true, don't format the categories derived from the inflection tags; just return them.
* `.sort`: Sort key for formatted categories. Ignored when `.no_format_categories` = {true}.
* `.nocat`: Suppress computation of categories (even if `.no_format_categories` is not given).
* `.notext`: Disable display of all tag text and `inflection of` text. (FIXME: Maybe not implemented correctly.)
* `.nolink`: Suppress linking of terms in inflection tags.
* `.capfirst`: Capitalize the first word displayed.
* `.pretext`: Additional text to display before the inflection tags, but after any top-level labels.
* `.posttext`: Additional text to display after the lemma links.
* `.text_classes`: CSS classes used to wrap the tag text and lemma links. Default is
{"form-of-definition use-with-mention"}.
* `.lemma_classes`: Additional CSS classes used to wrap the lemma links. Default is {"form-of-definition-link"}.
* `.joiner`: Override the joiner (normally a slash) used to join multipart tags. You should normally not specify this.
* `.nowrap`: Do not wrap the form-of definitions in a span, unless `.text_classes` is specified.
* `.ok_to_destructively_modify`: If set, data structures (including the nested lemma structures) can be modified
in-place to save memory; otherwise they will be copied before modifying.
A typical call might look like this (for {{m+|es|amo}}): {
local lang = require("Module:languages").getByCode("es")
local lemma_obj = {
lang = lang,
term = "amar",
}
return m_form_of.tagged_inflections({
lang = lang, tags = {"1", "s", "pres", "ind"}, lemmas = {lemma_obj}, lemma_face = "term", POS = "verb"
})
}
Normally, one value is returned, the formatted text, which has appended to it the formatted categories derived from the
tag-set-related categories generated by the specs in [Module:form of/cats]]. To suppress this, set
`data.no_format_categories` = {true}, in which case two values are returned, the formatted text without any formatted
categories appended and a list of the categories to be formatted.
NOTE: There are two sets of categories that may be generated: (1) categories derived directly from the tag sets, as
specified in [[Module:form of/cats]]; (2) categories derived from tag-set labels, either (a) set explicitly by the
caller in `data.tag_sets`, (b) specified by the user using `<lb:...>` attached to the last tag in a tag set, or
(c) specified in [[Module:form of/cats]]. The second type (label-related categories) are currently not returned in
the second return value of {tagged_inflections()}, and are currently inserted into the output text even if
`data.no_format_categories` is set to {true}; but they can be suppressed by setting `data.nocat` = {true} (which also
suppresses the first type of categories, those derived directly from tag sets, even if `data.no_format_categories` is
set to {true}).]==]
function export.tagged_inflections(data)
if not data.tags and not data.tag_sets then
error("First argument must be a table of arguments, and `.tags` or `.tag_sets` must be specified")
end
if data.tags and data.tag_sets then
error("Both `.tags` and `.tag_sets` cannot be specified")
end
local tag_sets = data.tag_sets
if not tag_sets then
tag_sets = split_tag_set(data.tags)
for i, tag_set in ipairs(tag_sets) do
tag_sets[i] = parse_tag_set_properties(tag_set)
end
end
local inflections = {}
local categories = {}
for _, tag_set in ipairs(tag_sets) do
local normalized_tag_sets = normalize_tag_set(tag_set.tags, data.lang, "do-track")
for _, normalized_tag_set in ipairs(normalized_tag_sets) do
local this_categories, this_labels = fetch_categories_and_labels(normalized_tag_set, data.lang,
data.POS, data.pagename, type(data.lemmas) == "table" and data.lemmas or nil)
if not data.nocat then
extend(categories, this_categories)
end
local cur_infl = get_tag_set_display_form(normalized_tag_set, data.lang, data.joiner, data.nolink)
if #cur_infl > 0 then
if tag_set.labels then
this_labels = append(tag_set.labels, this_labels)
end
insert(inflections, {infl_text = cur_infl, labels = this_labels})
end
end
end
local overall_labels, need_per_tag_set_labels
for _, inflection in ipairs(inflections) do
if overall_labels == nil then
overall_labels = inflection.labels
elseif not deep_equals(overall_labels, inflection.labels) then
need_per_tag_set_labels = true
overall_labels = nil
break
end
end
if not need_per_tag_set_labels then
for _, inflection in ipairs(inflections) do
inflection.labels = nil
end
end
local format_data = shallow_copy(data)
local of_text = data.lemmas and " of" or ""
local formatted_text, this_categories
if #inflections == 1 then
if need_per_tag_set_labels then
error("Internal error: need_per_tag_set_labels should not be set with one inflection")
end
format_data.text = format_labels(overall_labels, data, data.notext) .. (data.pretext or "") .. (data.notext and "" or
((data.capfirst and ucfirst(inflections[1].infl_text) or inflections[1].infl_text) .. of_text))
formatted_text, this_categories = format_form_of(format_data)
else
format_data.text = format_labels(overall_labels, data, data.notext) .. (data.pretext or "") .. (data.notext and "" or
((data.capfirst and "Inflection" or "inflection") .. of_text))
format_data.posttext = (data.posttext or "") .. ":"
local link
link, this_categories = format_form_of(format_data)
local text_classes = data.text_classes
if text_classes == nil and not data.nowrap then
text_classes = "form-of-definition use-with-mention"
end
for i, inflection in ipairs(inflections) do
inflections[i] = "\n## " .. format_labels(inflection.labels, data, false) ..
wrap_in_span(inflection.infl_text, text_classes)
end
formatted_text = link .. concat(inflections)
end
if this_categories[1] then
extend(categories, this_categories)
end
if not data.no_format_categories then
if categories[1] then
formatted_text = formatted_text .. format_categories(categories, data.lang,
data.sort, nil, export.force_cat)
end
return formatted_text
end
return formatted_text, categories
end
function export.dump_form_of_data(frame)
local data = {
require(form_of_data1_module),
require(form_of_data2_module)
}
return require(json_module).toJSON(data)
end
export.form_of_cats_module = form_of_cats_module
export.form_of_data1_module = form_of_data1_module
export.form_of_data2_module = form_of_data2_module
export.form_of_functions_module = form_of_functions_module
export.form_of_lang_data_module_prefix = form_of_lang_data_module_prefix
export.headword_data_module = headword_data_module -- so all form-of modules stay in sync
return export
6109jl733hkaom7j87d7vgalbzwidff
Mòideal:form of/templates
828
16707
86195
2026-07-19T23:17:51Z
Altronic
4137
Copy from English Wiktionary
86195
Scribunto
text/plain
local export = {}
local debug_track_module = "Module:debug/track"
local form_of_module = "Module:form of"
local functions_module = "Module:fun"
local headword_data_module = "Module:headword/data"
local languages_module = "Module:languages"
local load_module = "Module:load"
local parameters_module = "Module:parameters"
local parameter_utilities_module = "Module:parameter utilities"
local parse_interface_module = "Module:parse interface"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local insert = table.insert
local ipairs = ipairs
local pairs = pairs
local require = require
local boolean_param = {type = "boolean"}
-- FIXME: Finish [[Module:format utilities]].
-- local allowed_conj_set = require(format_utilities_module).allowed_conj_set_for_join_segments
local allowed_conj_set = {["and"] = true, ["or"] = true, ["and/or"] = true}
--[==[
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 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 format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function format_form_of(...)
format_form_of = require(form_of_module).format_form_of
return format_form_of(...)
end
local function get_lang(...)
get_lang = require(languages_module).getByCode
return get_lang(...)
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 load_data(...)
load_data = require(load_module).load_data
return load_data(...)
end
local function parse_inline_modifiers(...)
parse_inline_modifiers = require(parse_interface_module).parse_inline_modifiers
return parse_inline_modifiers(...)
end
local function pattern_escape(...)
pattern_escape = require(string_utilities_module).pattern_escape
return pattern_escape(...)
end
local function process_params(...)
process_params = require(parameters_module).process
return process_params(...)
end
local function safe_load_data(...)
safe_load_data = require(load_module).safe_load_data
return safe_load_data(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function split_tag_set(...)
split_tag_set = require(form_of_module).split_tag_set
return split_tag_set(...)
end
local function tagged_inflections(...)
tagged_inflections = require(form_of_module).tagged_inflections
return tagged_inflections(...)
end
local function trim(...)
trim = require(string_utilities_module).trim
return trim(...)
end
local function ucfirst(...)
ucfirst = require(string_utilities_module).ucfirst
return ucfirst(...)
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 force_cat
local function get_force_cat()
force_cat, get_force_cat = require(form_of_module).force_cat, nil
return force_cat
end
local m_form_of_pos
local function get_m_form_of_pos()
m_form_of_pos, get_m_form_of_pos = load_data(headword_data_module).pos_aliases, nil
return m_form_of_pos
end
local module_prefix
local function get_module_prefix()
module_prefix, get_module_prefix = require(form_of_module).form_of_lang_data_module_prefix, nil
return module_prefix
end
--[==[ intro:
This module contains code that directly implements {{tl|form of}}, {{tl|inflection of}}, and the various other
[[:Category:Form-of templates|form-of templates]]. It is meant to be called directly from templates. See also
[[Module:form of]], which contains the underlying implementing code and is meant to be called from other modules.
]==]
-- Add tracking category for PAGE when called from TEMPLATE. The tracking category linked to is
-- [[Wiktionary:Tracking/form of/TEMPLATE/PAGE]]. If TEMPLATE is omitted, the tracking category is of the form
-- [[Wiktionary:Tracking/form of/PAGE]].
local function track(page, template)
debug_track("form of/" .. (template and template .. "/" or "") .. page)
end
local function get_common_template_params()
return {
-- Named params not controlling link display
["cat"] = {list = true, sublist = "comma without whitespace", flatten = true},
["notext"] = boolean_param,
["sort"] = true,
["enclitic"] = true,
-- FIXME! The following should only be available when withcap=1 in invocation args or when withencap=1 and the
-- language is "en". Before doing that, need to remove all uses of nocap= in other circumstances.
["nocap"] = boolean_param,
["addl"] = true, -- additional text to display at the end, before the closing </span>
["pagename"] = true, -- for testing, etc.
}
end
-- Split TAGSPECS (inflection tag specifications) on SPLIT_REGEX, which
-- may be nil for no splitting.
local function split_inflection_tags(tagspecs, split_regex)
if not split_regex then
return tagspecs
end
local inflection_tags = {}
for _, tagspec in ipairs(tagspecs) do
for tag in gsplit(tagspec, split_regex) do
insert(inflection_tags, tag)
end
end
return inflection_tags
end
local function parse_terms_with_inline_modifiers(paramname, val, param_mods, lang)
local function generate_obj(term)
return {lang = lang, term = decode_entities(term)}
end
return parse_inline_modifiers(val, {
paramname = paramname,
param_mods = param_mods,
generate_obj = generate_obj,
splitchar = ",",
outer_container = {},
})
end
-- Need to do what [[Module:parameters]] does to string arguments from parent_args as we're running this
-- before calling [[Module:parameters]] on parent_args.
local function ine(arg)
if not arg then
return nil
end
arg = trim(arg)
return arg ~= "" and arg or nil
end
local function add_base_lemma_params(parent_args, iargs, params, compat)
-- Check the language-specific data for additional base lemma params. But if there's no language-specific data,
-- attempt any parent varieties as well (i.e. superordinate varieties).
local lang = get_lang(ine(parent_args[compat and "lang" or 1]) or ine(iargs.lang) or "und", nil, true)
while lang do
local langdata = safe_load_data((module_prefix or get_module_prefix()) .. lang:getCode())
if langdata then
local base_lemma_params = langdata.base_lemma_params
if base_lemma_params then
for _, param in ipairs(base_lemma_params) do
params[param.param] = true
end
return base_lemma_params
end
end
lang = lang:getParent()
end
end
--[=[
Modify `params` in-place by adding parameters that control the link to the main entry and any base lemmas. `term_param`
is the number of the param specifying the main entry itself; `term_param` + 1 will be the display text, and `term_param`
+ 2 will be the gloss, unless `no_numbered_gloss` is given. The base lemma parameters are determined by attempting to
load language-specific data for the language of the page; this comes from |lang= if `compat` is given, else from |1=.
]=]
local function add_link_and_base_lemma_params(iargs, parent_args, params, term_param, compat, no_numbered_gloss)
local base_lemma_params
if not iargs.nolink and not iargs.linktext then
-- Numbered params controlling link display
if iargs.with_multiple_parts then
params[term_param] = {list = true, allow_holes = true}
else
params[term_param + 1] = {alias_of = "alt"}
if not no_numbered_gloss then
params[term_param + 2] = {alias_of = "t"}
end
params[term_param] = true
end
base_lemma_params = add_base_lemma_params(parent_args, iargs, params, compat)
end
return base_lemma_params
end
local function handle_withdot_withcap(iargs, params)
local ignored_tracked_params = {}
if iargs.withdot then
params.dot = true
params.nodot = boolean_param
end
if iargs.withcap and iargs.withencap then
error("Internal error: Can specify only one of withcap= and withencap=")
end
if not iargs.withcap then
params.cap = boolean_param
ignored_tracked_params.nocap = iargs.withencap and "non-english" or "always"
end
return ignored_tracked_params
end
--[=[
Construct and return the full definition line for a form-of-type template invocation. `data` is an object with the
following fields:
* `template`: Approximate template name, for debug tracking;
* `iargs`: processed invocation arguments;
* `parent_args`: raw parent args from `frame:getParent().args`;
* `params`: partially constructed params structure of the sort passed to `process()` in [[Module:parameters]], but
without any link params;
* `ignored_tracked_params`: params that are ignored but should be tracked, to be eventually removed;
* `term_param`: the parent argument specifying the main entry;
* `compat`: true if the language code is found in args.lang instead of args[1];
* `base_lemma_params`: if non-nil, a list of base lemma param objects of the sort stored in the language-specific data;
* `do_form_of`: a function of one argument, `lemma_data`, that returns the actual definition-line text and any
language-specific categories. See below.
This function does several things:
# If link parameters are called for (neither `iargs.nolink` nor `iargs.linktext` are given), augment the `params`
structure with separate link parameters.
# Modify the parent args as appropriate if invocation arguments def= or ignore= are given.
# Parse the parent args, both for separate parameter properties and inline modifiers on the term parameter (which may
consist of multiple comma-separated terms).
# Compute categories to add to the page, including language-specific categories and any categories requested by the
invocation or parent args.
# Parse enclitic and extra base lemma parameters.
# Construct the actual text using `do_form_of`.
# Add a terminating period/dot as appropriate, along with the formatted categories.
`do_form_of` takes one argument, `lemma_data`, which looks like this:
{
lang = LANG,
args = {ARG = VALUE, ARG = VALUE, ...},
lemmas = {LEMMA_OBJ, LEMMA_OBJ, ...},
enclitics = {ENCLITIC_OBJ, ENCLITIC_OBJ, ...},
base_lemmas = {BASE_LEMMA_OBJ, BASE_LEMMA_OBJ, ...},
categories = {"CATEGORY", "CATEGORY", ...},
posttext = "POSTTEXT" or nil,
}
where
* LANG is the language code;
* ARGS is the parsed arguments, based on what the user specified;
* LEMMAS is a sequence of objects specifying the main entries/lemmas, as passed to full_link in [[Module:links]];
however, if the invocation argument linktext= is given, it will be a string consisting of that text, and if the
invocation argument nolink= is given, it will be nil;
* ENCLITICS is nil or a sequence of objects specifying the enclitics, as passed to full_link in [[Module:links]];
* BASE_LEMMA_OBJ is a sequence of objects specifying the base lemma(s), which are used when the lemma is itself a
form of another lemma (the base lemma), e.g. a comparative, superlative or participle; each object is of the form
{ paramobj = PARAM_OBJ, lemmas = {LEMMA_OBJ, LEMMA_OBJ, ...} } where PARAM_OBJ describes the properties of the
base lemma parameter (i.e. the relationship between the intermediate and base lemmas) and LEMMA_OBJ is of the same
format of ENCLITIC_OBJ, i.e. an object suitable to be passed to full_link in [[Module:links]]; PARAM_OBJ is of the
format { param = "PARAM", tags = {"TAG", "TAG", ...} } where PARAM is the name of the parameter to
{{inflection of}} etc. that holds the base lemma(s) of the specified relationship and the tags describe the
relationship, such as {"comd"} or {"past", "part"};
* CATEGORIES is the categories to add the page to (consisting of any categories specified in the invocation or
parent args and any tracking categories, but not any additional lang-specific categories that may be added by
{{inflection of}} or similar templates);
* POSTTEXT is text to display at the end of the form-of text, before the final </span> (or at the end of the first
line, before the colon, in a multiline {{infl of}} call).
`do_form_of` should return two arguments:
(1) The actual definition-line text, marked up appropriately with <span>...</span> but without any terminating
period/dot.
(2) Any extra categories to add the page to (other than those that can be derived from parameters specified to the
invocation or parent arguments, which will automatically be added to the page).
]=]
local function construct_form_of_text(data)
local template, iargs, parent_args, params, no_numbered_gloss, do_form_of =
data.template, data.iargs, data.parent_args, data.params, data.no_numbered_gloss, data.do_form_of
local term_param = iargs.term_param
local compat = iargs.lang or parent_args.lang
term_param = term_param or compat and 1 or 2
-- Numbered params
params[compat and "lang" or 1] = {
required = not iargs.lang,
type = "language",
default = iargs.lang or "und"
}
-- Error to catch most uses of old-style parameters for {{contraction of}}. (FIXME: Remove eventually.)
if iargs.with_multiple_parts then
if ine(parent_args[term_param + 2]) and not ine(parent_args[term_param + 1]) and not ine(parent_args.tr2) and not ine(parent_args.ts2)
and not ine(parent_args.t2) and not ine(parent_args.gloss2) and not ine(parent_args.g2)
and not ine(parent_args.alt2) then
error(("You specified a term in %s= and not one in %s=. You probably meant to use t= to specify a gloss instead. "
.. "If you intended to specify two terms, put the second term in %s=."):format(term_param + 2, term_param + 1,
term_param + 1))
end
if not ine(parent_args[term_param + 1]) and not ine(parent_args.alt2) and not ine(parent_args.tr2) and not ine(parent_args.ts2)
and ine(parent_args.g2) then
error(("You specified a gender in g2= but no term in %s=. You were probably trying to specify two genders for "
.. "a single term. To do that, put both genders in g=, comma-separated."):format(term_param + 1))
end
end
local base_lemma_params = add_link_and_base_lemma_params(iargs, parent_args, params, term_param, compat,
no_numbered_gloss)
local ignored_tracked_params = handle_withdot_withcap(iargs, params)
--[=[
Process parent arguments. This is similar to the following:
require("Module:parameters").process(parent_args, params)
but in addition it does the following:
(1) Supplies default values for unspecified parent arguments as specified in DEFAULTS, which consist of specs of the
form "ARG=VALUE". These are added to the parent arguments prior to processing, so boolean and number parameters
will process the value appropriately.
(2) Removes parent arguments specified in IGNORESPECS, which consist either of bare argument names to remove, or
list-argument names to remove of the form "ARG:list".
(3) Tracks the use of any parent arguments specified in TRACKED_PARAMS, which is a set where the keys are arguments
as they exist after processing (hence numeric arguments should be numbers, not strings) and the values should be
boolean true.
]=]--
local defaults = iargs.def
local ignorespecs = iargs.ignore
if defaults[1] or ignorespecs[1] then
local new_parent_args = {}
for _, default in ipairs(defaults) do
local defparam, defval = default:match("^(.-)=(.*)$")
if not defparam then
error("Bad default spec " .. default)
end
new_parent_args[defparam] = defval
end
local params_to_ignore = {}
local numbered_list_params_to_ignore = {}
local named_list_params_to_ignore = {}
for _, ignorespec in ipairs(ignorespecs) do
for ignore in gsplit(ignorespec, ",") do
local param = ignore:match("^(.*):list$")
if param then
if param:match("^%d+$") then
insert(numbered_list_params_to_ignore, tonumber(param))
else
insert(named_list_params_to_ignore, "^" .. pattern_escape(param) .. "%d*$")
end
else
if ignore:match("^%d+$") then
ignore = tonumber(ignore)
end
params_to_ignore[ignore] = true
end
end
end
for k, v in pairs(parent_args) do
if not params_to_ignore[k] then
local ignore_me = false
if type(k) == "number" then
for _, lparam in ipairs(numbered_list_params_to_ignore) do
if k >= lparam then
ignore_me = true
break
end
end
else
for _, lparam in ipairs(named_list_params_to_ignore) do
if k:match(lparam) then
ignore_me = true
break
end
end
end
if not ignore_me then
new_parent_args[k] = v
end
end
end
parent_args = new_parent_args
end
local param_mods_with_conj, param_mods_without_conj
local function get_param_mods(with_conj)
if with_conj then
if not param_mods_with_conj then
param_mods_with_conj = require(parameter_utilities_module).construct_param_mods {
{group = {"link", "q", "l", "ref"}},
{param = "conj", set = allowed_conj_set, overall = true},
}
end
return param_mods_with_conj
else
if not param_mods_without_conj then
param_mods_without_conj = require(parameter_utilities_module).construct_param_mods {
{group = {"link", "q", "l", "ref"}},
-- Override lit= to have a separate overall value
{param = "lit", separate_no_index = true},
}
end
return param_mods_without_conj
end
end
local terms, args
if iargs.nolink or iargs.linktext then
args = process_params(parent_args, params)
else
local m_param_utils = require(parameter_utilities_module)
if iargs.with_multiple_parts then
terms, args = m_param_utils.parse_list_with_inline_modifiers_and_separate_params {
params = params,
param_mods = get_param_mods(false),
raw_args = parent_args,
termarg = term_param,
track_module = "form of" .. (template and "/" .. template or ""),
lang = compat and "lang" or 1,
sc = "sc.default",
parse_lang_prefix = true,
default_separator = " +‎ ",
special_separators = {[";"] = " or "},
}
else
terms, args = m_param_utils.parse_term_with_inline_modifiers_and_separate_params {
params = params,
param_mods = get_param_mods("with conj"),
raw_args = parent_args,
termarg = term_param,
track_module = "form of" .. (template and "/" .. template or ""),
lang = compat and "lang" or 1,
sc = "sc",
parse_lang_prefix = true,
make_separate_g_into_list = true,
splitchar = ",",
subitem_param_handling = "last",
}
end
end
local lang = args[compat and "lang" or 1]
-- Tracking for certain user-specified params. This is generally used for
-- parameters that we accept but ignore, so that we can eventually remove
-- all uses of these params and stop accepting them.
if ignored_tracked_params then
for ignored_tracked_param, condition in pairs(ignored_tracked_params) do
if parent_args[ignored_tracked_param] and (condition ~= "non-english" or lang:getCode() ~= "en") then
track("arg/" .. ignored_tracked_param, template)
track("arg/" .. ignored_tracked_param)
end
end
end
-- Determine categories for the page, including tracking categories
local categories = {}
if not args.nocat then
for _, cat in ipairs(iargs.cat) do
insert(categories, lang:getFullName() .. " " .. cat)
end
end
for _, cat in ipairs(args.cat) do
insert(categories, lang:getFullName() .. " " .. cat)
end
-- Format the link, preceding text and categories
local function add_term_tracking_categories(term)
-- add tracking category if term is same as page title
if term and mw.loadData("Module:headword/data").pagename == lang:stripDiacritics(term) then
insert(categories, "Forms linking to themselves")
end
-- maybe add tracking category if primary entry doesn't exist (this is an
-- expensive call so we don't do it by default)
if iargs.noprimaryentrycat and term and mw.title.getCurrentTitle().nsText == ""
and not mw.title.new(term):getContent() then
insert(categories, lang:getFullName() .. " " .. iargs.noprimaryentrycat)
end
end
local lemmas
if iargs.nolink then
lemmas = nil
elseif iargs.linktext then
lemmas = iargs.linktext
else
if iargs.with_multiple_parts then
lemmas = terms
else
lemmas = terms.terms
end
if not lemmas[1] then
if mw.title.getCurrentTitle().nsText == "Template" then
lemmas[1] = {
lang = lang,
term = "term"
}
else
error("No linked-to term specified")
end
end
for _, termobj in ipairs(lemmas) do
if termobj.term then
add_term_tracking_categories(termobj.term)
end
-- NOTE: Formerly, template arg sc= overrode inline modifier <sc:...>, which seems backwards, so I've
-- changed it. Hopefully nothing depended on the old behavior.
end
end
local enclitics, enclitic_conj
if args.enclitic then
local enclitics_obj = parse_terms_with_inline_modifiers("enclitic", args.enclitic, get_param_mods("with conj"),
lang)
enclitics = enclitics_obj.terms
enclitic_conj = enclitics_obj.conj
end
local base_lemmas = {}
if base_lemma_params then
for _, base_lemma_param_obj in ipairs(base_lemma_params) do
local param = base_lemma_param_obj.param
if args[param] then
local base_lemmas_obj = parse_terms_with_inline_modifiers(param, args[param],
get_param_mods("with conj"), lang)
insert(base_lemmas, {
paramobj = base_lemma_param_obj,
lemmas = base_lemmas_obj.terms,
conj = base_lemmas_obj.conj,
})
end
end
end
local posttext = iargs.posttext
local addl = args.addl
if addl then
posttext = posttext or ""
if addl:find("^[;:]") then
posttext = posttext .. addl
elseif addl:find("^_") then
posttext = posttext .. " " .. addl:sub(2)
else
posttext = posttext .. ", " .. addl
end
end
local conj
if iargs.with_multiple_parts then
conj = false -- signals to use 'separator' field
else
conj = terms and terms.conj or iargs.conj
end
local lemma_data = {
lang = lang,
args = args,
lemmas = lemmas,
conj = conj,
enclitics = enclitics,
enclitic_conj = enclitic_conj,
base_lemmas = base_lemmas,
categories = categories,
posttext = posttext,
lit = iargs.with_multiple_parts and args.lit.default or nil,
}
local form_of_text, lang_cats = do_form_of(lemma_data)
extend(lemma_data.categories, lang_cats)
local text = form_of_text .. (
args.nodot and "" or args.dot or iargs.withdot and "." or ""
)
if not lemma_data.categories[1] then
return text
end
return text .. format_categories(lemma_data.categories, lemma_data.lang, args.sort,
-- If lemma_is_sort_key is given, supply the first lemma term as the sort base if possible. If sort= is given,
-- it will override the base; otherwise, the base will be converted appropriately to a sort key using the
-- same algorithm applied to pagenames.
iargs.lemma_is_sort_key and type(lemma_data.lemmas) == "table" and lemma_data.lemmas[1].term,
-- Supply the first lemma's script for sort key computation.
force_cat or get_force_cat(), type(lemma_data.lemmas) == "table" and lemma_data.lemmas[1].sc)
end
-- Invocation parameters shared between form_of_t(), tagged_form_of_t() and inflection_of_t(). `calling_func` is the
-- invoking function ("form_of_t", "tagged_form_of_t" or "inflection_of_t"), for handling parameters shared by two but
-- not all three invoking functions.
local function get_common_invocation_params(calling_func)
local iparams = {
["term_param"] = {type = "number"},
["lang"] = true, -- To be used as the default code in params.
["sc"] = {type = "script"},
["cat"] = {list = true, sublist = "comma without whitespace", flatten = true},
["ignore"] = {list = true},
["def"] = {list = true},
["conj"] = {set = allowed_conj_set, default = "and"},
["withcap"] = boolean_param,
["withencap"] = boolean_param,
["withdot"] = boolean_param,
["nolink"] = boolean_param,
["linktext"] = true,
["posttext"] = true,
["noprimaryentrycat"] = true,
["lemma_is_sort_key"] = true,
}
if calling_func ~= "inflection_of_t" then
iparams.with_multiple_parts = boolean_param
end
return iparams
end
local function should_ucfirst_text(args, iargs, lang)
local code = lang:getCode()
return args.cap or (iargs.withcap or iargs.withencap and code == "en" or code == "mul") and not args.nocap
end
--[==[
Function that implements {{tl|form of}} and the various more specific form-of templates (but not {{tl|inflection of}}
or templates that take tagged inflection parameters).
Invocation params:
; {{para|1|req=1}}
: Text to display before the link.
; {{para|term_param}}
: Numbered param holding the term linked to. Other numbered params come after. Defaults to 1 if invocation or template
param {{para|lang}} is present, otherwise 2.
; {{para|with_multiple_parts|1}}
: If specified, higher numbered parameters above the numbered param in {{para|term_param}} specify additional parts out
of which the term was constructed, for use by templates like {{tl|contraction of}}. (If not specified, the parameter
at <code><var>term_param</var> + 1</code> is the display form, same as {{para|alt}}, and the parameter at
at <code><var>term_param</var> + 2</code> is the gloss, same as {{para|t}}.)
; {{para|lang}}
: Default language code for language-specific templates. If specified, no language code needs to be specified, and if
specified it needs to be set using {{para|lang}}, not {{para|1}}.
; {{para|sc}}
: Default script code for language-specific templates. The script code can still be overridden using template param
{{para|sc}}.
; {{para|cat}}, {{para|cat2}}, ...:
: Categories to place the page into. The language name will automatically be prepended. Note that there is also a
template param {{para|cat}} to specify categories at the template level. Use of {{para|nocat}} disables categorization
of categories specified using invocation param {{para|cat}}, but not using template param {{para|cat}}. A single param
can specify multiple comma-separated categories if no space follows the comma.
; {{para|ignore}}, {{para|ignore2}}, ...:
: One or more template params to silently accept and ignore. Useful e.g. when the template takes additional parameters
such as {{para|from}} or {{para|POS}}. Each value is a comma-separated list of either bare parameter names or
specifications of the form `PARAM:list` to specify that the parameter is a list parameter.
; {{para|def}}, {{para|def2}}, ...:
: One or more default values to supply for template args. For example, specifying {{para|def|2=tr=-}} causes the default
for template param {{para|tr}} to be `-`. Actual template params override these defaults.
; {{para|withcap|1}}
: Capitalize the first character of the text preceding the link, unless template param {{para|nocap}} is given.
; {{para|withencap|1}}
: Capitalize the first character of the text preceding the link if the language is English and template param
{{para|nocap}} is not given.
; {{para|withdot|1}}
: Add a final period after the link, unless template param {{para|nodot}} is given to suppress the period, or
{{para|dot}} is given to specify an alternative punctuation character.
; {{para|nolink|1}}
: Suppress the display of the link. If specified, none of the template params that control the link
({{para|<var>term_param</var>}}, {{para|<var>term_param</var> + 1}}, {{para|<var>term_param</var> + 2}}, {{para|t}},
{{para|gloss}}, {{para|sc}}, {{para|tr}}, {{para|ts}}, {{para|pos}}, {{para|g}}, {{para|id}}, {{para|lit}}) will be
available. If the calling template uses any of these parameters, they must be ignored using {{para|ignore}}.
{{para|linktext}}
: Override the display of the link with the specified text. This is useful if a custom template is available to format
the link (e.g. in Hebrew, Chinese and Japanese). If specified, none of the template params that control the link
({{para|<var>term_param</var>}}, {{para|<var>term_param</var> + 1}}, {{para|<var>term_param</var> + 2}}, {{para|t}},
{{para|gloss}}, {{para|sc}}, {{para|tr}}, {{para|ts}}, {{para|pos}}, {{para|g}}, {{para|id}}, {{para|lit}}) will be
available. If the calling template uses any of these parameters, they must be ignored using {{para|ignore}}.
; {{para|posttext}}
: Additional text to display directly after the formatted link, before any terminating period/dot and inside of
`<span class='use-with-mention'>`.
; {{para|noprimaryentrycat}}
: Category to add the page to if the primary entry linked to doesn't exist. The language name will automatically be
prepended.
; {{para|lemma_is_sort_key|1}}
: If the user didn't specify a sort key, use the lemma as the sort key (instead of the page itself).
]==]
function export.form_of_t(frame)
local iparams = get_common_invocation_params("form_of_t")
iparams[1] = {required = true}
local iargs = process_params(frame.args, iparams)
local parent_args = frame:getParent().args
local params = get_common_template_params()
if next(iargs.cat) then
params.nocat = boolean_param
end
return construct_form_of_text {
template = "form-of-t",
iargs = iargs,
parent_args = parent_args,
params = params,
do_form_of = function(lemma_data)
local args = lemma_data.args
local text
if args.notext then
text = ""
else
text = iargs[1]
if should_ucfirst_text(args, iargs, lemma_data.lang) then
text = ucfirst(text)
end
end
-- This returns two values, which we pass up to the caller.
return format_form_of {
lang = lemma_data.lang, text = text, lemmas = lemma_data.lemmas, conj = lemma_data.conj,
enclitics = lemma_data.enclitics, base_lemmas = lemma_data.base_lemmas, lemma_face = "term",
lit = lemma_data.lit, posttext = lemma_data.posttext
}
end
}
end
--[=[
Construct and return the full definition line for a form-of-type template invocation that is based on inflection tags.
This is a wrapper around construct_form_of_text() and takes the following arguments: processed invocation arguments
IARGS, processed parent arguments ARGS, TERM_PARAM (the parent argument specifying the main entry), COMPAT (true if the
language code is found in args.lang instead of args[1]), and TAGS, the list of (non-canonicalized) inflection tags.
It returns that actual definition-line text including terminating period/full-stop, formatted categories, etc. and
should be directly returned as the template function's return value.
]=]
local function construct_tagged_form_of_text(data)
local template, iargs, parent_args, params, no_numbered_gloss, tags =
data.template, data.iargs, data.parent_args, data.params, data.no_numbered_gloss, data.tags
-- Named params not controlling link display
-- Always included because lang-specific categories may be added
params.nocat = boolean_param
params.p = true
params.POS = {alias_of = "p"}
return construct_form_of_text {
template = template,
iargs = iargs,
parent_args = parent_args,
params = params,
no_numbered_gloss = no_numbered_gloss,
do_form_of = function(lemma_data)
local args = lemma_data.args
if is_callable(tags) then
tags = tags(args)
end
-- NOTE: tagged_inflections returns two values, so we do too.
return tagged_inflections {
lang = lemma_data.lang,
tags = tags,
lemmas = lemma_data.lemmas,
conj = lemma_data.conj,
enclitics = lemma_data.enclitics,
enclitic_conj = lemma_data.enclitic_conj,
base_lemmas = lemma_data.base_lemmas,
lemma_face = "term",
POS = args.p,
pagename = args.pagename,
-- Set no_format_categories because we do it ourselves in construct_form_of_text().
no_format_categories = true,
nocat = args.nocat,
notext = args.notext,
capfirst = should_ucfirst_text(args, iargs, lemma_data.lang),
posttext = lemma_data.posttext,
}
end
}
end
--[==[
Function that implements form-of templates that are defined by specific tagged inflections (typically a template
referring to a non-lemma inflection, such as {{tl|plural of}}). This works exactly like {form_of_t()} except that the
"form of" text displayed before the link is based off of a pre-specified set of inflection tags (which will be
appropriately linked to the glossary) instead of arbitrary text. From the user's perspective, there is no difference
between templates implemented using {form_of_t()} and {tagged_form_of_t()}; they accept exactly the same parameters and
work the same. See also {inflection_of_t()} below, which is intended for templates with user-specified inflection tags.
Invocation params:
; {{para|1|req=1}}, {{para|2}}, ...
: One or more inflection tags describing the inflection in question.
; {{para|split_tags}}
: If specified, character to split specified inflection tags on. This allows multiple tags to be included in a single
argument, simplifying template code.
; {{para|term_param}}
; {{para|with_multiple_parts|1}}
; {{para|lang}}
; {{para|sc}}
; {{para|cat}}, {{para|cat2}}, ...
; {{para|ignore}}, {{para|ignore2}}, ...
; {{para|def}}, {{para|def2}}, ...
; {{para|withcap|1}}
; {{para|withencap|1}}
; {{para|withdot|1}}
; {{para|nolink|1}}
; {{para|linktext}}
; {{para|posttext}}
; {{para|noprimaryentrycat}}
; {{para|lemma_is_sort_key|1}}
: All of these are the same as in {form_of_t()}.
]==]
function export.tagged_form_of_t(frame)
local iparams = get_common_invocation_params("tagged_form_of_t")
iparams[1] = {list = true, required = true}
iparams.split_tags = true
local iargs = process_params(frame.args, iparams)
local parent_args = frame:getParent().args
local params = get_common_template_params()
return construct_tagged_form_of_text {
template = "tagged-form-of-t",
iargs = iargs,
parent_args = parent_args,
params = params,
tags = split_inflection_tags(iargs[1], iargs.split_tags),
}
end
--[==[
Function that implements {{tl|inflection of}} and certain semi-specific variants, such as {{tl|participle of}} and
{{tl|past participle form of}}. This function is intended for templates that allow the user to specify a set of
inflection tags.
It works similarly to {form_of_t()} and {tagged_form_of_t()} except that the calling convention for the calling
template is
: { {{TEMPLATE|LANG|MAIN_ENTRY_LINK|MAIN_ENTRY_DISPLAY_TEXT|TAG|TAG|...}}}
instead of
: { {{TEMPLATE|LANG|MAIN_ENTRY_LINK|MAIN_ENTRY_DISPLAY_TEXT|GLOSS}}}
Note that there isn't a numbered parameter for the gloss, but it can still be specified using {{para|t}} or
{{para|gloss}}.
Invocation params:
; {{para|preinfl}}, {{para|preinfl2}}, ...
: Extra inflection tags to automatically prepend to the tags specified by the template.
; {{para|postinfl}}, {{para|postinfl2}}, ...
: Extra inflection tags to automatically append to the tags specified by the template. Used for example by
{{tl|past participle form of}} to add the tags `of the|past|p` onto the user-specified tags, which indicate which
past participle form the page refers to.
; {{para|split_tags}}
: If specified, character to split specified inflection tags on. This allows multiple tags to be included in a single
argument, simplifying template code. Note that this applies *ONLY* to inflection tags specified in the invocation
arguments using {{para|preinfl}} or {{para|postinfl}}, not to user-specified inflection tags.
; {{para|term_param}}
; {{para|lang}}
; {{para|sc}}
; {{para|cat}}, {{para|cat2}}, ...
; {{para|ignore}}, {{para|ignore2}}, ...
; {{para|def}}, {{para|def2}}, ...
; {{para|withcap|1}}
; {{para|withencap|1}}
; {{para|withdot|1}}
; {{para|nolink|1}}
; {{para|linktext}}
; {{para|posttext}}
; {{para|noprimaryentrycat}}
; {{para|lemma_is_sort_key|1}}
: All of these are the same as in {form_of_t()}.
]==]
function export.inflection_of_t(frame)
local iparams = get_common_invocation_params("inflection_of_t")
iparams.preinfl = {list = true}
iparams.postinfl = {list = true}
iparams.split_tags = true
local iargs = process_params(frame.args, iparams)
local parent_args = frame:getParent().args
local params = get_common_template_params()
local compat = iargs.lang or parent_args.lang
local tagsind = (iargs.term_param or compat and 1 or 2) + 2
params[tagsind] = {list = true,
-- at least one inflection tag is required unless preinfl or postinfl tags are given
required = #iargs.preinfl == 0 and #iargs.postinfl == 0}
return construct_tagged_form_of_text {
template = "inflection-of-t",
iargs = iargs,
parent_args = parent_args,
params = params,
no_numbered_gloss = true,
tags = function(args)
local infls
if not next(iargs.preinfl) and not next(iargs.postinfl) then
-- If no preinfl or postinfl tags, just use the user-specified tags directly.
infls = args[tagsind]
else
-- Otherwise, we need to prepend the preinfl tags and postpend the postinfl tags. If there's only one tag set
-- (no semicolon), it's easier. Since this is common, we optimize for it.
infls = {}
local saw_semicolon = false
for _, infl in ipairs(args[tagsind]) do
if infl == ";" then
saw_semicolon = true
break
end
end
local split_preinfl = split_inflection_tags(iargs.preinfl, iargs.split_tags)
local split_postinfl = split_inflection_tags(iargs.postinfl, iargs.split_tags)
if not saw_semicolon then
extend(infls, split_preinfl)
extend(infls, args[tagsind])
extend(infls, split_postinfl)
else
local groups = split_tag_set(args[tagsind])
for _, group in ipairs(groups) do
if #infls > 0 then
insert(infls, ";")
end
extend(infls, split_preinfl)
extend(infls, group)
extend(infls, split_postinfl)
end
end
end
return infls
end,
}
end
--[==[
Normalize a part-of-speech tag given a possible abbreviation (passed in as {{para|1}} of the invocation args). If the
abbreviation isn't recognized, the original POS tag is returned. If no POS tag is passed in, return the value of
invocation arg {{para|default}}.
]==]
function export.normalize_pos(frame)
local iparams = {
[1] = true,
["default"] = true,
}
local iargs = process_params(frame.args, iparams)
if not iargs[1] and not iargs.default then
error("Either 1= or default= must be given in the invocation args")
end
if not iargs[1] then
return iargs.default
end
return (m_form_of_pos or get_m_form_of_pos())[iargs[1]] or iargs[1]
end
return export
qngi7g0ngefkunhqwao6b90egp7n7t7
Mòideal:parse interface
828
16708
86196
2026-07-19T23:19:19Z
Altronic
4137
Copy from English Wiktionary
86196
Scribunto
text/plain
local export = {}
local string_utilities_module = "Module:string utilities"
local parse_utilities_module = "Module:parse utilities"
local table_module = "Module:table"
--[=[
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 rfind(...)
rfind = require(string_utilities_module).find
return rfind(...)
end
local function rsplit(...)
rsplit = require(string_utilities_module).split
return rsplit(...)
end
local function split_on_comma(...)
split_on_comma = require(parse_utilities_module).split_on_comma
return split_on_comma(...)
end
local function split_escaping(...)
split_escaping = require(parse_utilities_module).split_escaping
return split_escaping(...)
end
local function parse_inline_modifiers(...)
parse_inline_modifiers = require(parse_utilities_module).parse_inline_modifiers
return parse_inline_modifiers(...)
end
local function parse_term_with_lang(...)
parse_term_with_lang = require(parse_utilities_module).parse_term_with_lang
return parse_term_with_lang(...)
end
local function term_contains_top_level_html(...)
term_contains_top_level_html = require(parse_utilities_module).term_contains_top_level_html
return term_contains_top_level_html(...)
end
local function escape_comma_whitespace(...)
escape_comma_whitespace = require(parse_utilities_module).escape_comma_whitespace
return escape_comma_whitespace(...)
end
local function unescape_comma_whitespace(...)
unescape_comma_whitespace = require(parse_utilities_module).unescape_comma_whitespace
return unescape_comma_whitespace(...)
end
local function shallow_copy(...)
shallow_copy = require(table_module).shallowCopy
return shallow_copy(...)
end
local function decode_entities(...)
-- FIXME: Why are we doing this? It was added to [[Module:form of/templates]] in
-- https://en.wiktionary.org/w/index.php?title=Module:form_of/templates&diff=prev&oldid=81900806 on 2024-09-24
-- by [[User:Theknightwho]] with the comment "Optimisations + decode HTML entities.".
--
-- NOTE: We could add a check for & in the term before calling decode_entities(), but in practice,
-- [[Module:string utilities]] is essentially always loaded so there's little point.
str_decode_entities = require(string_utilities_module).decode_entities
return str_decode_entities(...)
end
--[==[
This is an almost drop-in replacement for split_on_comma() in [[Module:parse utilities]], with optimizations to avoid
loading and running the while algorithm in [[Module:parse utilities]] except when necessary.
]==]
function export.split_on_comma(val)
if val:find(",%s") or (val:find(",") and val:find("[\\%[<]")) then
-- Comma after whitespace not split; nor are backslash-escaped commas or commas inside of square or
-- angle brackets. If we see any of these, use the more sophisticated algorithm in
-- [[Module:parse utilities]]. Otherwise it's safe to just split on commas directly. This optimization
-- avoids loading [[Module:parse utilities]] unnecessarily.
return split_on_comma(val)
else
return rsplit(val, ",")
end
end
--[==[
This is similar to parse_term_with_lang() in [[Module:parse utilities]], but if there is no colon + non-space in the
term, it will be returned directly and not parsed into link/display format. If you need the link/display arguments
even in the absence of a language prefix, call [[Module:parse utilities]] directly.
]==]
function export.parse_term_with_lang(data)
if data.term:find(":[^ ]") then
return parse_term_with_lang(data)
else
return data.term, nil, nil, nil
end
end
--[==[
This is an almost drop-in replacement for parse_inline_modifiers() in [[Module:parse utilities]] except that
# it won't attempt to parse inline modifiers if it detects that top-level HTML is present (but it will still split on
`splitchar` if given, unless it detects the presence of the {{tl|,}} template);
# it has a default for `generate_obj` that simply sets `lang` and `term` after calling `decode_entities()` on the term
(FIXME: this was inherited from code added to [[Module:form of/templates]] by [[User:Theknightwho]]; I don't know why
it is necessary);
# it has a lot of optimizations to avoid loading [[Module:parse utilities]] in simple cases where there are no `<` signs and (when `splitchar` is given) either there are no delimiters present at all or no characters present that will make
a simple split on `splitchar` invalid.
Generally you should use this in preference to either calling parse_inline_modifiers() directly in
[[Module:parse utilities]] or rolling your own front-end function.
]==]
function export.parse_inline_modifiers(val, props)
local paramname, lang, splitchar = props.paramname, props.lang, props.splitchar
local preserve_splitchar, escape_fun, unescape_fun = props.preserve_splitchar, props.escape_fun, props.unescape_fun
local outer_container = props.outer_container
local generate_obj = props.generate_obj or function(term)
return {lang = lang, term = decode_entities(term)}
end
local delimiter_key = props.delimiter_key or "delimiter"
-- Check for inline modifier, e.g. מרים<tr:Miryem>. But exclude HTML entry with <span ...>, <i ...>, <br/> or
-- similar in it, caused by wrapping an argument in {{l|...}}, {{af|...}} or similar. Basically, all tags of
-- the sort we parse here should consist of a less-than sign, plus letters, plus a colon, e.g. <tr:...>, so if
-- we see a tag on the outer level that isn't in this format, we don't try to parse it. The restriction to the
-- outer level is to allow generated HTML inside of e.g. qualifier tags, such as foo<q:similar to {{m|fr|bar}}>.
if val:find("<") and not term_contains_top_level_html(val) then
if not props.generate_obj then
props = shallow_copy(props)
props.generate_obj = generate_obj
end
return parse_inline_modifiers(val, props)
end
if not splitchar then
return generate_obj(val)
end
local retval
if splitchar == "," and escape_fun == nil and unescape_fun == nil then
if val:find(",</") then
-- This happens when there's an embedded {{,}} template, as in [[MMR]], [[TMA]], [[DEI]], where an
-- initialism expands to multiple terms; easiest not to try and parse the lemma spec as multiple lemmas.
retval = {val}
else
retval = export.split_on_comma(val)
end
for i, split in ipairs(retval) do
retval[i] = generate_obj(split)
if preserve_splitchar and i > 1 then
retval[delimiter_key] = ","
end
end
elseif rfind(val, splitchar) then
if val:find(",</") then
-- This happens when there's an embedded {{,}} template, as in [[MMR]], [[TMA]], [[DEI]], where an
-- initialism expands to multiple terms; easiest not to try and parse the lemma spec as multiple lemmas.
retval = {val}
elseif escape_fun or unescape_fun or val:find(",%s") or val:find("[\\%[<]") then
local defaulted_escape_fun, defaulted_unescape_fun
if escape_fun == nil then
defaulted_escape_fun = escape_comma_whitespace
end
if unescape_fun == nil then
defaulted_unescape_fun = unescape_comma_whitespace
end
retval = split_escaping(val, splitchar, preserve_splitchar, defaulted_escape_fun, defaulted_unescape_fun)
elseif preserve_splitchar then
retval = rsplit(val, "(" .. splitchar .. ")")
else
retval = rsplit(val, splitchar)
end
if preserve_splitchar then
local new_retval = {}
for j = 1, #retval, 2 do
local obj = generate_obj(retval[j])
if j > 1 then
obj[delimiter_key] = retval[j - 1]
end
table.insert(new_retval, obj)
end
retval = new_retval
else
for i, split in ipairs(retval) do
retval[i] = generate_obj(split)
end
end
else
retval = {generate_obj(val)}
end
if outer_container then
outer_container.terms = retval
return outer_container
end
return retval
end
return export
bq5sys0ifgirhdamppe6s3lw9xawfoo
Mòideal:parse utilities
828
16709
86197
2026-07-19T23:23:56Z
Altronic
4137
Copy from English Wiktionary
86197
Scribunto
text/plain
local export = {}
local fun_is_callable_module = "Module:fun/isCallable"
local languages_module = "Module:languages"
local parameters_module = "Module:parameters"
local string_char_module = "Module:string/char"
local string_utilities_module = "Module:string utilities"
local table_insert_if_not_module = "Module:table/insertIfNot"
local assert = assert
local concat = table.concat
local dump = mw.dumpObject
local error = error
local insert = table.insert
local ipairs = ipairs
local list_to_text = mw.text.listToText
local pairs = pairs
local require = require
local sort = table.sort
local type = type
local ugsub = mw.ustring.gsub
local function convert_val(...)
convert_val = require(parameters_module).convert_val
return convert_val(...)
end
local function get_lang(...)
get_lang = require(languages_module).getByCode
return get_lang(...)
end
local function insert_if_not(...)
insert_if_not = require(table_insert_if_not_module)
return insert_if_not(...)
end
local function is_callable(...)
is_callable = require(fun_is_callable_module)
return is_callable(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function u(...)
u = require(string_char_module)
return u(...)
end
local function umatch(...)
umatch = require(string_utilities_module).match
return umatch(...)
end
--[==[ intro:
In order to understand the following parsing code, you need to understand how inflected text specs work. They are
intended to work with inflected text where individual words to be inflected may be followed by inflection specs in
angle brackets. The format of the text inside of the angle brackets is up to the individual language and part-of-speech
specific implementation. A real-world example is as follows: `<nowiki>[[медичний|меди́чна]]<+> [[сестра́]]<*,*#.pr></nowiki>`.
This is the inflection of the Ukrainian multiword expression {{m|uk|меди́чна сестра́||nurse|lit=medical sister}},
consisting of two words: the adjective {{m|uk|меди́чна||medical|pos=feminine singular}} and the noun {{m|uk|сестра́||sister}}.
The specs in angle brackets follow each word to be inflected; for example, `<+>` means that the preceding word should be
declined as an adjective.
The code below works in terms of balanced expressions, which are bounded by delimiters such as `< >` or `[ ]`. The
intention is to allow separators such as spaces to be embedded inside of delimiters; such embedded separators will not
be parsed as separators. For example, Ukrainian noun specs allow footnotes in brackets to be inserted inside of angle
brackets; something like `меди́чна<+> сестра́<pr.[this is a footnote]>` is legal, as is
`<nowiki>[[медичний|меди́чна]]<+> [[сестра́]]<pr.[this is an <i>italicized footnote</i>]></nowiki>`, and the parsing code
should not be confused by the embedded brackets, spaces or angle brackets.
The parsing is done by two functions, which work in close concert: {parse_balanced_segment_run()} and
{split_alternating_runs()}. To illustrate, consider the following:
{parse_balanced_segment_run("foo<M.proper noun> bar<F>", "<", ">")} =<br />
{ {"foo", "<M.proper noun>", " bar", "<F>", ""}}
then
{split_alternating_runs({"foo", "<M.proper noun>", " bar", "<F>", ""}, " ")} =<br />
{ {{"foo", "<M.proper noun>", ""}, {"bar", "<F>", ""}}}
Here, we start out with a typical inflected text spec `foo<M.proper noun> bar<F>`, call {parse_balanced_segment_run()} on
it, and call {split_alternating_runs()} on the result. The output of {parse_balanced_segment_run()} is a list where
even-numbered segments are bounded by the bracket-like characters passed into the function, and odd-numbered segments
consist of the surrounding text. {split_alternating_runs()} is called on this, and splits '''only''' the odd-numbered
segments, grouping all segments between the specified character. Note that the inner lists output by
{split_alternating_runs()} are themselves in the same format as the output of {parse_balanced_segment_run()}, with
bracket-bounded text in the even-numbered segments. Hence, such lists can be passed again to {split_alternating_runs()}.
]==]
--[==[
Parse a string containing matched instances of parens, brackets or the like. Return a list of strings, alternating
between textual runs not containing the open/close characters and runs beginning and ending with the open/close
characters. For example,
{parse_balanced_segment_run("foo(x(1)), bar(2)", "(", ")") = {"foo", "(x(1))", ", bar", "(2)", ""}}
]==]
function export.parse_balanced_segment_run(segment_run, open, close)
return split(segment_run, "(%b" .. open .. close .. ")")
end
-- The following is an equivalent, older implementation that does not use %b (written before I was aware of %b).
--[=[
function export.parse_balanced_segment_run(segment_run, open, close)
local break_on_open_close = split(segment_run, "([%" .. open .. "%" .. close .. "])")
local text_and_specs = {}
local level = 0
local seg_group = {}
for i, seg in ipairs(break_on_open_close) do
if i % 2 == 0 then
if seg == open then
insert(seg_group, seg)
level = level + 1
else
assert(seg == close)
insert(seg_group, seg)
level = level - 1
if level < 0 then
error("Unmatched " .. close .. " sign: '" .. segment_run .. "'")
elseif level == 0 then
insert(text_and_specs, concat(seg_group))
seg_group = {}
end
end
elseif level > 0 then
insert(seg_group, seg)
else
insert(text_and_specs, seg)
end
end
if level > 0 then
error("Unmatched " .. open .. " sign: '" .. segment_run .. "'")
end
return text_and_specs
end
]=]
--[==[
Like parse_balanced_segment_run() but accepts multiple sets of delimiters. For example,
{parse_multi_delimiter_balanced_segment_run("foo[bar(baz[bat])], quux<glorp>", {{"[", "]"}, {"(", ")"}, {"<", ">"}}) =
{"foo", "[bar(baz[bat])]", ", quux", "<glorp>", ""}}.
Each element in the list of delimiter pairs is a string specifying an equivalence class of possible delimiter
characters. You can use this, for example, to allow either "[" or "&#91;" to be treated equivalently, with either
one closed by either "]" or "&#93;". To do this, first replace "&#91;" and "&#93;" with single Unicode
characters such as U+FFF0 and U+FFF1, and then specify a two-character string containing "[" and U+FFF0 as the opening
delimiter, and a two-character string containing "]" and U+FFF1 as the corresponding closing delimiter.
If `no_error_on_unmatched` is given and an error is found during parsing, a string is returned containing the error
message instead of throwing an error.
]==]
function export.parse_multi_delimiter_balanced_segment_run(segment_run, delimiter_pairs, no_error_on_unmatched)
local escaped_delimiter_pairs = {}
local open_to_close_map = {}
local open_close_items = {}
local open_items = {}
for _, open_close in ipairs(delimiter_pairs) do
local open, close = open_close[1], open_close[2]
open = open:gsub("([%[%]%%%%-])", "%%%1")
close = close:gsub("([%[%]%%%%-])", "%%%1")
insert(open_close_items, open)
insert(open_close_items, close)
insert(open_items, open)
open = "[" .. open .. "]"
close = "[" .. close .. "]"
open_to_close_map[open] = close
insert(escaped_delimiter_pairs, {open, close})
end
local open_close_pattern = "([" .. concat(open_close_items) .. "])"
local open_pattern = "([" .. concat(open_items) .. "])"
local break_on_open_close = split(segment_run, open_close_pattern)
local text_and_specs = {}
local level = 0
local seg_group = {}
local open_at_level_zero
for i, seg in ipairs(break_on_open_close) do
if i % 2 == 0 then
insert(seg_group, seg)
if level == 0 then
if not umatch(seg, open_pattern) then
local errmsg = "Unmatched close sign " .. seg .. ": '" .. segment_run .. "'"
if no_error_on_unmatched then
return errmsg
else
error(errmsg)
end
end
assert(open_at_level_zero == nil)
for _, open_close in ipairs(escaped_delimiter_pairs) do
local open = open_close[1]
if umatch(seg, open) then
open_at_level_zero = open
break
end
end
if open_at_level_zero == nil then
error(("Internal error: Segment %s didn't match any open regex"):format(seg))
end
level = level + 1
elseif umatch(seg, open_at_level_zero) then
level = level + 1
elseif umatch(seg, open_to_close_map[open_at_level_zero]) then
level = level - 1
assert(level >= 0)
if level == 0 then
insert(text_and_specs, concat(seg_group))
seg_group = {}
open_at_level_zero = nil
end
end
elseif level > 0 then
insert(seg_group, seg)
else
insert(text_and_specs, seg)
end
end
if level > 0 then
local errmsg = "Unmatched open sign " .. open_at_level_zero .. ": '" .. segment_run .. "'"
if no_error_on_unmatched then
return errmsg
else
error(errmsg)
end
end
return text_and_specs
end
--[==[
Check whether a term contains top-level HTML. We want to distinguish inline modifiers from HTML. We assume an inline
modifier is either a boolean modifier like `<bor>` or a prefix modifier like `<tr:Miryem>`. All other things inside of
angle brackets, e.g. `<nowiki><span class="foo"></nowiki>`, `<nowiki></span></nowiki>`, `<nowiki><br/></nowiki>`, etc.,
should be flagged as HTML (typically caused by wrapping an argument in {{tl|m|...}}, {{tl|af|...}} or similar, but
sometimes specified directly, e.g. `<nowiki><sup>6</sup></nowiki>`). By default, we assume the tag in an inline modifier
contains either letters, numbers, hyphens or underscore (but not spaces), and must either stand alone or be followed by
a colon, leading to a default HTML-checking pattern of {"<[%w_%-]*[^%w_%-:>]"}. But this can be modified; e.g.
[[Module:tl-pronunciation]] allows modifiers of the form `<<var>pos</var>^<var>defn</var>>` or
`<<var>pos</var>,<var>pos</var>,<var>pos</var>^<var>defn</var>>`, and would need to use its own HTML pattern. It's
important we restrict the check for HTML to top-level to allow for generated HTML inside of e.g. qualifier tags, such as
`<nowiki>foo<q:similar to {{m|fr|bar}}></nowiki>`.
]==]
function export.term_contains_top_level_html(term, html_pattern)
html_pattern = html_pattern or "<[%w_%-]*[^%w_%-:>]"
-- If no HTML anywhere, the answer is no.
if not term:find(html_pattern) then
return false
end
-- Otherwise, we have to call parse_balanced_segment_run() and check alternate runs at top level.
local runs = export.parse_balanced_segment_run(term, "<", ">")
for i = 2, #runs, 2 do
if runs[i]:find("^" .. html_pattern) then
return true
end
end
return false
end
--[==[
Check whether a term appears to have already been passed through `full_link()`. Passing it again will mangle it in
various ways; at best it will have unnecessary lang/script wrapping, which might do nothing but might result in
overly large fonts or other issues. We also check for uses of {{tl|ja-r/args}}, {{tl|ryu-r/args}} or {{tl|ko-l/args}},
which will be manged by `full_link()`. If this check succeeds, use the text raw instead of passing through
`full_link()`.
]==]
function export.term_already_linked(term)
return term:find("<span") or term:find("{{ja%-r|") or term:find("{{ryu%-r|") or term:find("{{ko%-l|")
end
--[==[
Split a list of alternating textual runs of the format returned by `parse_balanced_segment_run` on `splitchar`. This
only splits the odd-numbered textual runs (the portions between the balanced open/close characters). The return value
is a list of lists, where each list contains an odd number of elements, where the even-numbered elements of the sublists
are the original balanced textual run portions. For example, if we do
{parse_balanced_segment_run("foo<M.proper noun> bar<F>", "<", ">") =
{"foo", "<M.proper noun>", " bar", "<F>", ""}}
then
{split_alternating_runs({"foo", "<M.proper noun>", " bar", "<F>", ""}, " ") =
{{"foo", "<M.proper noun>", ""}, {"bar", "<F>", ""}}}
Note that we did not touch the text "<M.proper noun>" even though it contains a space in it, because it is an
even-numbered element of the input list. This is intentional and allows for embedded separators inside of
brackets/parens/etc. Note also that the inner lists in the return value are of the same form as the input list (i.e.
they consist of alternating textual runs where the even-numbered segments are balanced runs), and can in turn be passed
to split_alternating_runs().
If `preserve_splitchar` is passed in, the split character is included in the output, as follows:
{split_alternating_runs({"foo", "<M.proper noun>", " bar", "<F>", ""}, " ", true) =
{{"foo", "<M.proper noun>", ""}, {" "}, {"bar", "<F>", ""}}}
Consider what happens if the original string has multiple spaces between brackets, and multiple sets of brackets
without spaces between them.
{parse_balanced_segment_run("foo[dated][low colloquial] baz-bat quux xyzzy[archaic]", "[", "]") =
{"foo", "[dated]", "", "[low colloquial]", " baz-bat quux xyzzy", "[archaic]", ""}}
then
{split_alternating_runs({"foo", "[dated]", "", "[low colloquial]", " baz-bat quux xyzzy", "[archaic]", ""}, "[ %-]") =
{{"foo", "[dated]", "", "[low colloquial]", ""}, {"baz"}, {"bat"}, {"quux"}, {"xyzzy", "[archaic]", ""}}}
If `preserve_splitchar` is passed in, the split character is included in the output,
as follows:
{split_alternating_runs({"foo", "[dated]", "", "[low colloquial]", " baz bat quux xyzzy", "[archaic]", ""}, "[ %-]", true) =
{{"foo", "[dated]", "", "[low colloquial]", ""}, {" "}, {"baz"}, {"-"}, {"bat"}, {" "}, {"quux"}, {" "}, {"xyzzy", "[archaic]", ""}}}
As can be seen, the even-numbered elements in the outer list are one-element lists consisting of the separator text.
]==]
function export.split_alternating_runs(segment_runs, splitchar, preserve_splitchar)
local grouped_runs = {}
local run = {}
for i, seg in ipairs(segment_runs) do
if i % 2 == 0 then
insert(run, seg)
else
local parts = split(seg, preserve_splitchar and "(" .. splitchar .. ")" or splitchar)
insert(run, parts[1])
for j=2,#parts do
insert(grouped_runs, run)
run = {parts[j]}
end
end
end
if #run > 0 then
insert(grouped_runs, run)
end
return grouped_runs
end
--[==[
After calling `parse_multi_delimiter_balanced_segment_run()`, rejoin delimiter-bounded textual runs (i.e. textual runs
surrounded by certain matched delimiters) with the runs on either side. This can be used when some of the matched
delimiters are specified only in order to ensure that delimiters inside of other delimiters aren't parsed. As an
example, [[Module:object usage]] calls
{m_parse_utilities.parse_multi_delimiter_balanced_segment_run(object, {{"[", "]"}, {"(", ")"}, {"<", ">"}})} but the
actual syntax of {{tl|+obj}} only uses parens and angle brackets as delimiters. Square brackets are included so that
internal links are treated as units (i.e. parens and angle brackets occurring inside of them aren't parsed), but beyond
that we don't treat square brackets as delimiters, so we want to rejoin square-bracket-delimited textual runs with
adjacent runs before further parsing.
There are two primary workflows when using this function:
# If you only care about balanced delimiters occurring inside of other balanced delimiters (e.g. in the above example
with [[Module:object usage]], you can call `rejoin_delimited_runs()` directly after
`parse_multi_delimiter_balanced_segment_run()`.
# However, if you care about single delimiters such as commas and slashes occurring inside of balanced delimiters (e.g.
if you allow multiple comma-separated terms, e.g. of which can have associated inline modifiers, and you don't want
commas inside of internal links to be treated as delimiters), you need to call `rejoin_delimited_runs()` ''after''
calling `split_alternating_runs()`. This is used, for example, in `parse_inline_modifiers()` for exactly this reason,
when a `splitchar` is provided.
`data` is an object of properties. Currently there are two: `runs` (the output of calling
`parse_multi_delimiter_balanced_segment_run()`, i.e. a list of textual runs, where even-numbered elements begin and end
with a matched delimiter and odd-numbered elements are surrounding text) and `delimiter_pattern` (a Lua pattern matching
delimited textual runs that we want to rejoin with the surrounding text). `delimiter_pattern` should normally be
anchored at the beginning; e.g. {"^%["} would be the correct pattern to use when rejoining square-bracket-delimited
textual runs, as described above.
]==]
function export.rejoin_delimited_runs(data)
local joined_runs = {}
local i = 1
while i <= #data.runs do
local run = data.runs[i]
if i % 2 == 0 and run:find(data.delimiter_pattern) then
joined_runs[#joined_runs] = joined_runs[#joined_runs] .. run .. data.runs[i + 1]
i = i + 2
else
insert(joined_runs, run)
i = i + 1
end
end
return joined_runs
end
function export.strip_spaces(text)
return (ugsub(text, "^%s*(.-)%s*$", "%1"))
end
--[==[
Apply an arbitrary function `frob` to the "raw-text" segments in a split run set (the output of
split_alternating_runs()). We leave alone stuff within balanced delimiters (footnotes, inflection specs and the
like), as well as splitchars themselves if present. `preserve_splitchar` indicates whether splitchars are present
in the split run set. `frob` is a function of one argument (the string to frob) and should return one argument (the
frobbed string). We operate by only frobbing odd-numbered segments, and only in odd-numbered runs if
preserve_splitchar is given.
]==]
function export.frob_raw_text_alternating_runs(split_run_set, frob, preserve_splitchar)
for i, run in ipairs(split_run_set) do
if not preserve_splitchar or i % 2 == 1 then
for j, segment in ipairs(run) do
if j % 2 == 1 then
run[j] = frob(segment)
end
end
end
end
end
--[==[
Like split_alternating_runs() but applies an arbitrary function `frob` to "raw-text" segments in the result (i.e.
not stuff within balanced delimiters such as footnotes and inflection specs, and not splitchars if present). `frob`
is a function of one argument (the string to frob) and should return one argument (the frobbed string).
]==]
function export.split_alternating_runs_and_frob_raw_text(run, splitchar, frob, preserve_splitchar)
local split_runs = export.split_alternating_runs(run, splitchar, preserve_splitchar)
export.frob_raw_text_alternating_runs(split_runs, frob, preserve_splitchar)
return split_runs
end
--[==[
FIXME: Older entry point. Call `split_alternating_runs_and_frob_raw_text()` in [[Module:parse utilities]] directly.
Like `split_alternating_runs()` but strips spaces from both ends of the odd-numbered elements (only in odd-numbered runs
if `preserve_splitchar` is given). Effectively we leave alone the footnotes and splitchars themselves, but otherwise
strip extraneous spaces. Spaces in the middle of an element are also left alone.
]==]
function export.split_alternating_runs_and_strip_spaces(segment_runs, splitchar, preserve_splitchar)
return export.split_alternating_runs_and_frob_raw_text(segment_runs, splitchar, export.strip_spaces, preserve_splitchar)
end
--[==[
Split the non-modifier parts of an alternating run (after parse_balanced_segment_run() is called) on a Lua pattern,
but not on certain sequences involving characters in that pattern (e.g. comma+whitespace). `splitchar` is the pattern
to split on; `preserve_splitchar` indicates whether to preserve the delimiter and is the same as in
split_alternating_runs(). `escape_fun` is called beforehand on each run of raw text and should return two values:
the escaped run and whether unescaping is needed. If any call to `escape_fun` indicates that unescaping is needed,
`unescape_fun` will be called on each run of raw text after splitting on `splitchar`. The return value of this
function is as in split_alternating_runs().
]==]
function export.split_alternating_runs_escaping(run, splitchar, preserve_splitchar, escape_fun, unescape_fun)
-- First replace comma with a temporary character in comma+whitespace sequences.
local need_unescape = false
for i in ipairs(run) do
if i % 2 == 1 and escape_fun then
local this_need_unescape
run[i], this_need_unescape = escape_fun(run[i])
need_unescape = need_unescape or this_need_unescape
end
end
if need_unescape then
return export.split_alternating_runs_and_frob_raw_text(run, splitchar, unescape_fun, preserve_splitchar)
else
return export.split_alternating_runs(run, splitchar, preserve_splitchar)
end
end
--[==[
Replace comma with a temporary char in comma + whitespace.
]==]
function export.escape_comma_whitespace(run, tempcomma)
tempcomma = tempcomma or u(0xFFF0)
local escaped = false
if run:find("\\,") then
-- FIXME: we should probably convert literal \\ to \ to allow people to put a backslash before a comma that
-- should be passed through; but maybe it's enough to use an HTML escape for the comma or backslash.
run = (run:gsub("\\,", tempcomma)) -- discard backslash before comma, doing its duty to protect the comma
escaped = true
end
if run:find(",%s") then
run = (run:gsub(",(%s)", tempcomma .. "%1"))
escaped = true
end
return run, escaped
end
--[==[
Undo the replacement of comma with a temporary char.
]==]
function export.unescape_comma_whitespace(run, tempcomma)
tempcomma = tempcomma or u(0xFFF0)
return (run:gsub(tempcomma, ","))
end
--[==[
Split the non-modifier parts of an alternating run (after parse_balanced_segment_run() is called) on comma, but not
on comma+whitespace. See `split_on_comma()` above for more information and the meaning of `tempcomma`.
]==]
function export.split_alternating_runs_on_comma(run, tempcomma)
tempcomma = tempcomma or u(0xFFF0)
-- Replace comma with a temporary char in comma + whitespace.
local function escape_comma_whitespace(seg)
return export.escape_comma_whitespace(seg, tempcomma)
end
-- Undo replacement of comma with a temporary char in comma + whitespace.
local function unescape_comma_whitespace(seg)
return export.unescape_comma_whitespace(seg, tempcomma)
end
return export.split_alternating_runs_escaping(run, ",", false, escape_comma_whitespace, unescape_comma_whitespace)
end
--[==[
Split text on a Lua pattern, but not on certain sequences involving characters in that pattern (e.g.
comma+whitespace). `splitchar` is the pattern to split on; `preserve_splitchar` indicates whether to preserve the
delimiter between split segments. `escape_fun` is called beforehand on the text and should return two values: the
escaped run and whether unescaping is needed. If the call to `escape_fun` indicates that unescaping is needed,
`unescape_fun` will be called on each run of text after splitting on `splitchar`. The return value of this a list
of runs, interspersed with delimiters if `preserve_splitchar` is specified.
]==]
function export.split_escaping(text, splitchar, preserve_splitchar, escape_fun, unescape_fun)
if not umatch(text, splitchar) then
return {text}
end
-- If there are square or angle brackets, we don't want to split on delimiters inside of them. To effect this, we
-- use parse_multi_delimiter_balanced_segment_run() to parse balanced brackets, then do delimiter splitting on the
-- non-bracketed portions of text using split_alternating_runs_escaping(), and concatenate back to a list of
-- strings. When calling parse_multi_delimiter_balanced_segment_run(), we make sure not to throw an error on
-- unbalanced brackets; in that case, we fall through to the code below that handles the case without brackets.
if text:find("[%[<]") then
local runs = export.parse_multi_delimiter_balanced_segment_run(text, {{"[", "]"}, {"<", ">"}},
"no error on unmatched")
if type(runs) ~= "string" then
local split_runs = export.split_alternating_runs_escaping(runs, splitchar, preserve_splitchar, escape_fun,
unescape_fun)
for i = 1, #split_runs do
split_runs[i] = concat(split_runs[i])
end
return split_runs
end
end
-- First escape sequences we don't want to count for splitting.
local need_unescape
if escape_fun then
text, need_unescape = escape_fun(text)
end
local parts = split(text, preserve_splitchar and "(" .. splitchar .. ")" or splitchar)
if need_unescape then
for i = 1, #parts, (preserve_splitchar and 2 or 1) do
parts[i] = unescape_fun(parts[i])
end
end
return parts
end
--[==[
Split text on comma, but not on comma+whitespace. This is similar to `mw.text.split(text, ",")` but will not split
on commas directly followed by whitespace, to handle embedded commas in terms (which are almost always followed by
a space). `tempcomma` is the Unicode character to temporarily use when doing the splitting; normally U+FFF0, but
you can specify a different character if you use U+FFF0 for some internal purpose.
]==]
function export.split_on_comma(text, tempcomma)
-- Don't do anything if no comma. Note that split_escaping() has a similar check at the beginning, so if there's a
-- comma we effectively do this check twice, but this is worth it to optimize for the common no-comma case.
if not text:find(",") then
return {text}
end
tempcomma = tempcomma or u(0xFFF0)
-- Replace comma with a temporary char in comma + whitespace.
local function escape_comma_whitespace(run)
return export.escape_comma_whitespace(run, tempcomma)
end
-- Undo replacement of comma with a temporary char in comma + whitespace.
local function unescape_comma_whitespace(run)
return export.unescape_comma_whitespace(run, tempcomma)
end
return export.split_escaping(text, ",", false, escape_comma_whitespace, unescape_comma_whitespace)
end
--[==[
Ensure that Wikicode (template calls, bracketed links, HTML, bold/italics, etc.) displays literally in error messages
by inserting a Unicode word-joiner symbol after all characters that may trigger Wikicode interpretation. Replacing
with equivalent HTML escapes doesn't work because they are displayed literally. I could not get this to work using
<nowiki>...</nowiki> (those tags display literally), using using {{#tag:nowiki|...}} (same thing) or using
mw.getCurrentFrame():extensionTag("nowiki", ...) (everything gets converted to a strip marker
`UNIQ--nowiki-00000000-QINU` or similar). FIXME: This is a massive hack; there must be a better way.
]==]
function export.escape_wikicode(term)
term = term:gsub("([%[<'{])", "%1" .. u(0x2060))
return term
end
function export.make_parse_err(arg_gloss)
return function(msg, stack_frames_to_ignore)
error(export.escape_wikicode(("%s: %s"):format(msg, arg_gloss)), stack_frames_to_ignore)
end
end
-- Parse a term that may include a link '[[LINK]]' or a two-part link '[[LINK|DISPLAY]]'. FIXME: Doesn't currently
-- handle embedded links like '[[FOO]] [[BAR]]' or [[FOO|BAR]] [[BAZ]]' or '[[FOO]]s'; if they are detected, it returns
-- the term unchanged and `nil` for the display form.
local function parse_bracketed_term(term, parse_err)
local inside = term:match("^%[%[(.*)%]%]$")
if inside then
if inside:find("%[%[") or inside:find("%]%]") then
-- embedded links, e.g. '[[FOO]] [[BAR]]'; FIXME: we should process them properly
return term, nil
end
local parts = split(inside, "|")
if #parts > 2 then
parse_err("Saw more than two parts inside a bracketed link")
end
return parts[1], parts[2]
end
return term, nil
end
--[==[
Parse a term that may have a language code (or possibly multiple plus-separated language codes, if
`data.allow_multiple` is given) preceding it (e.g. {la:minūtia} or {grc:[[σκῶρ|σκατός]]} or
{nan-hbl+hak:[[毋]][[知]]}). Return five arguments:
# the original prefixed term; in the case of a Wikipedia or Wikisource prefix followed by a two-part link, it is a
two-part link with the Wikipedia/Wikisource prefix moved inside the link; in the case of a Wikipedia or Wikisource
prefix followed by a redundant one-part link, the brackets are removed;
# the language object corresponding to the language code (possibly a family object if `data.allow_family` is given), or
a list of such objects if `data.allow_multiple` is given;
# the link if the unprefixed term is of the form <code>[[<var>link</var>|<var>display</var>]]</code> or of the form
<code>[[<var>link</var>]]</code>, otherwise the full unprefixed term;
# the display part if the term is of the form <code>[[<var>link</var>|<var>display</var>]]</code> or has a Wikipedia or
Wikisource prefix (in which case the part minus the prefix and any following language code will be returned, with
redundant brackets stripped), else {nil};
# {true} if the term has a Wikipedia/Wikisource prefix, else {false}.
Etymology-only languages are always allowed. This function also correctly handles Wikipedia prefixes (e.g.
{w:Abatemarco} or {w:it:Colle Val d'Elsa} or {lw:ru:Филарет}) and Wikisource prefixes (e.g. {s:Twelve O'Clock} or
{s:[[Walden/Chapter XVIII|Walden]]} or {s:fr:Perceval ou le conte du Graal} or {s:ro:[[Domnul Vucea|Mr. Vucea]]} or
{ls:ko:이상적 부인} or {ls:ko:[[조선 독립의 서#一. 槪論|조선 독립의 서]]}) and converts them into two-part links,
with the display form not including the Wikipedia or Wikisource prefix unless it was explicitly specified using a
two-part link as in {lw:ru:[[Филарет (Дроздов)|Митрополи́т Филаре́т]]} or
{ls:ko:[[조선 독립의 서#一. 槪論|조선 독립의 서]]}. The difference between {w:} ("Wikipedia") and {lw:} ("Wikipedia
link") is that the latter requires a language code and returns the corresponding language object; same for the
difference between {s:} ("Wikisource") and {ls:} ("Wikisource link").
NOTE: Embedded links are not correctly handled currently. If an embedded link is detected, the whole term is returned
as the link part (third argument), and the display part is nil. If you construct your own link from the link and
display parts, you must check for this.
The calling convention is to pass in a single argument `data` containing the following fields:
* `term`: The term to parse.
* `parse_err`: An optional function of one or two arguments to display an error. (The second argument to the function is
the number of stack frames to ignore when calling error(); if you declare your error function with only one argument,
things will still work fine.)
* `paramname`: If `parse_err` is omitted, this should be a string naming a parameter to display in the error message,
along with the term in question, and will be used to generate a `parse_err` function using `make_parse_err()`. (If
`paramname` is omitted, just the term itself appears in the error message.)
* `allow_multiple`: Allow multiple plus-separated language codes, e.g. {nan-hbl+hak:[[毋]][[知]]}. See above.
* `allow_family`: Allow family objects to appear in place of language codes.
* `allow_bad`: Don't throw an error on invalid language code prefixes; instead, include the prefix and colon as part of
the term. Note that if a prefix doesn't look like a language code (e.g. if it's a number), the code won't even try to
parse it as a language code, regardless of the `allow_bad` setting, but will always include it in the term.
* `lang_cache`: A table mapping language codes to language objects. If the value is `false`, the language code is
invalid. If specified, the cache will be consulted before calling `getByCode()` in [[Module:languages]], and the
result cached. If not specified, no cache will be used.
]==]
function export.parse_term_with_lang(data)
local term = data.term
local parse_err = data.parse_err or
data.paramname and export.make_parse_err(("%s=%s"):format(data.paramname, term)) or
export.make_parse_err(term)
-- Parse off an initial language code (e.g. 'la:minūtia' or 'grc:[[σκῶρ|σκατός]]'). First check for Wikipedia
-- prefixes ('w:Abatemarco' or 'w:it:Colle Val d'Elsa' or 'lw:zh:邹衡') and Wikisource prefixes
-- ('s:ro:[[Domnul Vucea|Mr. Vucea]]' or 'ls:ko:이상적 부인'). Wikipedia/Wikisource language codes follow a similar
-- format to Wiktionary language codes (see below). Here and below we don't parse if there's a space after the
-- colon (happens e.g. if the user uses {{desc|...}} inside of {{col}}, grrr ...).
local termlang, foreign_wiki, actual_term = term:match("^(l?[ws]):([a-z][a-z][a-z-]*):([^ ].*)$")
if not termlang then
termlang, actual_term = term:match("^([ws]):([^ ].*)$")
end
if termlang then
local wiki_links = termlang:find("^l")
local base_wiki_prefix = termlang:find("w$") and "w:" or "s:"
local wiki_prefix = base_wiki_prefix .. (foreign_wiki and foreign_wiki .. ":" or "")
local link, display = parse_bracketed_term(actual_term, parse_err)
if link:find("%[%[") or display and display:find("%[%[") then
-- FIXME, this should be handlable with the right parsing code
parse_err("Cannot have embedded brackets following a Wikipedia (w:... or lw:...) link; expand the term to a fully bracketed term w:[[LINK|DISPLAY]] or similar")
end
local lang = wiki_links and get_lang(foreign_wiki, parse_err, "allow etym") or nil
local prefixed_link = wiki_prefix .. link
if display then
return ("[[%s|%s]]"):format(prefixed_link, display), lang, prefixed_link, display, true
else
-- Return the link minus any language codes as the fourth term (display form). Previously we returned `actual_term`
-- but this causes problems with redundant Wikipedia links of the form `w:[[Dragon Ball Z]]`. Don't generate a
-- two-part link so you can specify a display form in 3=. Note that the fourth and fifth params are currently only
-- used in [[Module:quote]].
return prefixed_link, lang, prefixed_link, link, true
end
end
-- Wiktionary language codes are in one of the following formats, where 'x' is a lowercase letter and 'X' an
-- uppercase letter:
-- xx
-- xxx
-- xxx-xxx
-- xxx-xxx-xxx (esp. for protolanguages)
-- xx-xxx (for etymology-only languages)
-- xx-xxx-xxx (maybe? for etymology-only languages)
-- xx-XX (for etymology-only languages, where XX is a country code, e.g. en-US)
-- xxx-XX (for etymology-only languages, where XX is a country code)
-- xx-xxx-XX (for etymology-only languages, where XX is a country code)
-- xxx-xxx-XX (for etymology-only langauges, where XX is a country code, e.g. nan-hbl-PH)
-- Things like xxx-x+ (e.g. cmn-pinyin, cmn-tongyong)
-- VL., LL., etc.
--
-- We check the for nonstandard Latin etymology language codes separately, and otherwise make only the following
-- assumptions:
-- (1) There are one to three hyphen-separated components.
-- (2) The last component can consist of two uppercase ASCII letters; otherwise, all components contain only
-- lowercase ASCII letters.
-- (3) Each component must have at least two letters.
-- (4) The first component must have two or three letters.
local function is_possible_lang_code(code)
-- Special hack for Latin variants, which can have nonstandard etym codes, e.g. VL., LL.
if code:find("^[A-Z]L%.$") then
return true
end
return code:find("^([a-z][a-z][a-z]?)$") or
code:find("^[a-z][a-z][a-z]?%-[A-Z][A-Z]$") or
code:find("^[a-z][a-z][a-z]?%-[a-z][a-z]+$") or
code:find("^[a-z][a-z][a-z]?%-[a-z][a-z]+%-[A-Z][A-Z]$") or
code:find("^[a-z][a-z][a-z]?%-[a-z][a-z]+%-[a-z][a-z]+$")
end
local function get_by_code(code, allow_bad)
local lang
if data.lang_cache then
lang = data.lang_cache[code]
end
if lang == nil then
lang = get_lang(code, not allow_bad and parse_err or nil, "allow etym",
data.allow_family)
if data.lang_cache then
data.lang_cache[code] = lang or false
end
end
return lang or nil
end
if data.allow_multiple then
local termlang_spec
termlang_spec, actual_term = term:match("^([a-zA-Z.,+-]+):([^ ].*)$")
if termlang_spec then
termlang = split(termlang_spec, "[,+]")
local all_possible_code = true
for _, code in ipairs(termlang) do
if not is_possible_lang_code(code) then
all_possible_code = false
break
end
end
if all_possible_code then
local saw_nil = false
for i, code in ipairs(termlang) do
termlang[i] = get_by_code(code, data.allow_bad)
if not termlang[i] then
saw_nil = true
end
end
if saw_nil then
termlang = nil
else
term = actual_term
end
else
termlang = nil
end
end
else
termlang, actual_term = term:match("^([a-zA-Z.-]+):([^ ].*)$")
if termlang then
if is_possible_lang_code(termlang) then
termlang = get_by_code(termlang, data.allow_bad)
if termlang then
term = actual_term
end
else
termlang = nil
end
end
end
local link, display = parse_bracketed_term(term, parse_err)
return term, termlang, link, display, false
end
--[==[
Parse a term that may have inline modifiers attached (e.g. {rifiuti<q:plural-only>} or
{rinfusa<t:bulk cargo><lit:resupplying><qq:more common in the plural {{m|it|rinfuse}}>}).
* `arg` is the term to parse.
* `props` is an object holding further properties controlling how to parse the term (only `param_mods` and
`generate_obj` are required):
** `paramname` is the name of the parameter where `arg` comes from, or nil if this isn't available (it is used only in
error messages).
** `param_mods` is a table describing the allowed inline modifiers (see below).
** `generate_obj` is a function of one or two arguments that should parse the argument minus the inline modifiers and
return a corresponding parsed object (into which the inline modifiers will be rewritten). If declared with one
argument, that will be the raw value to parse; if declared with two arguments, the second argument will be the
`parse_err` function (see below).
** `parse_err` is an optional function of one argument (an error message) and should display the error message, along
with any desired contextual text (e.g. the argument name and value that triggered the error). If omitted, a default
function will be generated which displays the error along with the original value of `arg` (passed through
{escape_wikicode()} above to ensure that Wikicode (such as links) is displayed literally).
** `splitchar` is a Lua pattern. If specified, `arg` can consist of multiple delimiter-separated terms, each of which
may be followed by inline modifiers, and the return value will be a list of parsed objects instead of a single
object. Note that splitting on delimiters will not happen in certain protected sequences (by default
comma+whitespace; see below). The algorithm to split on delimiters is sensitive to inline modifier syntax and will
not be confused by delimiters inside of inline modifiers, which do not trigger splitting (whether or not contained
within protected sequences).
** `outer_container`, if specified, is used when multiple delimiter-separated terms are possible, and is the object
into which the list of per-term objects is stored (into the `terms` field) and into which any modifiers that are
given the `overall` property (see below) will be stored. If given, this value will be returned as the value of
{parse_inline_modifiers()}. If `outer_container` is not given, {parse_inline_modifiers()} will return the list of
per-term objects directly, and no modifier may have an `overall` property.
** `preserve_splitchar`, if specified, causes the actual delimiter matched by `splitchar` to be returned in the
parsed object describing the element that comes after the delimiter. The delimiter is stored in a key whose
name is controlled by `delimiter_key`, which defaults to "delimiter".
** `delimiter_key` controls the key into which the actual delimiter is written when `preserve_splitchar` is used.
See above.
** `escape_fun` and `unescape_fun` are as in split_escaping() and split_alternating_runs_escaping() above and
control the protected sequences that won't be split. By default, `escape_comma_whitespace` and
`unescape_comma_whitespace` are used, so that comma+whitespace sequences won't be split. Set to `false` to disable
escaping/unescaping.
** `pre_normalize_modifiers`, if specified, is a function of one argument, which can be used to "normalize" modifiers
prior to further parsing. This is used, for example, in [[Module:tl-pronunciation]] to convert modifiers of the
form `<noun^expectation; hope>` to `<t:noun^expectation; hope>`, so they can be processed as standard modifiers. It
is also used in [[Module:ar-verb]] to convert footnotes of the form `[rare]` to `<footnote:[rare]>`, to allow for
mixing bracketed footnotes and inline modifiers when overriding verbal nouns and such. It could similarly be used to
handle boolean modifiers like `<slb>` in {{tl|desc}} and convert them to a standard form `<slb:1>`. It runs just
before parsing out the modifier prefix and value, and is passed an object containing fields `modtext` (the
un-normalized modifier text, including surrounding angle brackets, or in some cases, text surrounded by other
delimiters such as square brackets, if `parse_inline_modifiers_from_segments()` is being called and the caller did
their own parsing of balanced segment runs) and `parse_err` (the passed-in or autogenerated function to signal an
error during parsing; a function of one argument, a message, which throws an error displaying that message). It
should return a single value, the normalized value of `modtext`, including surrounding angle brackets.
`param_mods` is a table describing allowed modifiers. The keys of the table are modifier prefixes and the values are
tables describing how to parse and store the associated modifier values. Here is a typical example, for an item that
takes the standard modifiers associated with `full_link()` in [[Module:links]], as well as left and right qualifiers
and labels:
{
local param_mods = {
alt = {},
t = {
-- [[Module:links]] expects the gloss in "gloss".
item_dest = "gloss",
},
gloss = {},
tr = {},
ts = {},
g = {
-- [[Module:links]] expects the genders in "g". `sublist = true` automatically splits on comma (optionally
-- with surrounding whitespace).
item_dest = "genders",
sublist = true,
},
pos = {},
lit = {},
id = {},
sc = {
-- Automatically parse as a script code and convert to a script object.
type = "script",
},
-- Qualifiers and labels
q = {
type = "qualifier",
},
qq = {
type = "qualifier",
},
l = {
type = "labels",
},
ll = {
type = "labels",
},
}
}
In the table values:
* `item_dest` specifies the destination key to store the object into (if not the same as the modifier key itself).
* `type`, `set`, `sublist` and `convert` have the same meaning as in [[Module:parameters]] and are used for converting
the object from the string form given by the user into the form needed for further processing. Note that `type` makes
use of additional properties that may be specified. Specifically, if {type = "language"}, the properties `family` and
`method` are also examined, and if {type = "family"} or {type = "script"}, the property `method` is examined.
* `store` describes how to store the converted modifier value into the parsed object. If omitted, the converted value
is simply written into the parsed object under the appropriate key; but an error is generated if the key already has
a value. (This means that multiple occurrences of a given modifier are allowed if `store` is given, but not
otherwise.) `store` can be one of the following:
** {"insert"}: the converted value is appended to the key's value using {insert()}; if the key has no value, it
is first converted to an empty list;
** {"insertIfNot"}: is similar but appends the value using {insertIfNot()} in [[Module:table]];
** {"insert-flattened"}, the converted value is assumed to be a list and the objects are appended one-by-one into the
key's existing value using {insert()};
** {"insertIfNot-flattened"} is similar but appends using {insertIfNot()} in [[Module:table]]; (WARNING: When using
{"insert-flattened"} and {"insertIfNot-flattened"}, if there is no existing value for the key, the converted value is
just stored directly. This means that future appends will side-effect that value, so make sure that the return value
of the conversion function for this key generates a fresh list each time.)
** a function of one argument, an object with the following properties:
*** `dest`: the object to write the value into;
*** `key`: the field where the value should be written;
*** `converted`: the (converted) value to write;
*** `raw_val`: the raw, user-specified value (a string);
*** `parse_err`: a function of one argument (an error string), which signals an error, and includes extra context in
the message about the modifier in question, the angle-bracket spec that includes the modifier in it, the overall
value, and (if `paramname` was given) the parameter holding the overall value.
* `overall` only applies if `splitchar` is given. In this case, the modifier applies to the entire argument rather than
to an individual term in the argument, and must occur after the last item separated by `splitchar`, instead of being
allowed to occur after any of them. The modifier will be stored into the outer container object, which must exist
(i.e. `outer_container` must have been given).
The return value of {parse_inline_modifiers()} depends on whether `splitchar` and `outer_container` have been given. If
neither is given, the return value is the object returned by `generate_obj`. If `splitchar` but not `outer_container` is
given, the return value is a list of per-term objects, each of which is generated by `generate_obj`. If both `splitchar`
and `outer_container` are given, the return value is the value of `outer_container` and the per-term objects are stored
into the `terms` field of this object.
]==]
function export.parse_inline_modifiers(arg, props)
local segments
local function rejoin_bracket_delimited_runs(segments)
return export.rejoin_delimited_runs {
runs = segments,
delimiter_pattern = "^%[.*%]$",
}
end
local rejoin_square_brackets_after_split = false
-- The following is an optimization. If we see a square bracket (normally a double square bracket internal link
-- [[...]]), we want to not treat delimiter characters inside (either <...> balanced delimiters or separators such
-- as commas) as delimiters. But this requires a more sophisticated and slower algorithm, and most of the time it
-- isn't needed because there are no square brackets. So we check for a square bracket and fall back to a simpler
-- algorithm otherwise (which, since it involves only a single balanced delimiter, can use the built-in %b() Lua
-- pattern syntax, which AFAIK is implemented in C).
if arg:find("%[") then
segments = export.parse_multi_delimiter_balanced_segment_run(arg, {{"[", "]"}, {"<", ">"}})
if not props.splitchar then
segments = rejoin_bracket_delimited_runs(segments)
else
rejoin_square_brackets_after_split = true
end
else
segments = export.parse_balanced_segment_run(arg, "<", ">")
end
local function verify_no_overall()
for _, mod_props in pairs(props.param_mods) do
if mod_props.overall then
error("Internal caller error: Can't specify `overall` for a modifier in `param_mods` unless `outer_container` property is given")
end
end
end
if not props.splitchar then
if props.outer_container then
error("Internal caller error: Can't specify `outer_container` property unless `splitchar` is given")
end
verify_no_overall()
return export.parse_inline_modifiers_from_segments {
group = segments,
group_index = nil,
separated_groups = nil,
arg = arg,
props = props,
}
else
local terms = {}
if props.outer_container then
props.outer_container.terms = terms
else
verify_no_overall()
end
local escape_fun = props.escape_fun
if escape_fun == nil then
escape_fun = export.escape_comma_whitespace
end
local unescape_fun = props.unescape_fun
if unescape_fun == nil then
unescape_fun = export.unescape_comma_whitespace
end
local separated_groups = export.split_alternating_runs_escaping(segments, props.splitchar,
props.preserve_splitchar, escape_fun, unescape_fun)
for j = 1, #separated_groups, (props.preserve_splitchar and 2 or 1) do
if rejoin_square_brackets_after_split then
separated_groups[j] = rejoin_bracket_delimited_runs(separated_groups[j])
end
local parsed = export.parse_inline_modifiers_from_segments {
group = separated_groups[j],
group_index = j,
separated_groups = separated_groups,
arg = arg,
props = props,
}
if props.preserve_splitchar and j > 1 then
parsed[props.delimiter_key or "delimiter"] = separated_groups[j - 1][1]
end
insert(terms, parsed)
end
if props.outer_container then
return props.outer_container
else
return terms
end
end
end
--[==[
Parse a single term that may have inline modifiers attached. This is a helper function of {parse_inline_modifiers()} but
is exported separately in case the caller needs to make their own call to {parse_balanced_segment_run()} (as in
[[Module:quote]], which splits on several matched delimiters simultaneously). It takes only a single argument, `data`,
which is an object with the following fields:
* `group`: A list of segments as output by {parse_balanced_segment_run()} (see the overall comment at the top of
[[Module:parse utilities]]), or one of the lists returned by calling {split_alternating_runs()}.
* `separated_groups`: The list of groups (each of which is of the form of `group`) describing all the terms in the
argument parsed by {parse_inline_modifiers()}, or {nil} if this isn't applicable (i.e. multiple terms aren't allowed
in the argument). Currently used only the check the number of groups in the list against `group_index`.
* `group_index`: The index into `separated_groups` where `group` can be found, or {nil} if not applicable (see below).
* `arg`: The original user-specified argument being parsed; used only for error messages and only when `props.parse_err`
is not specified.
* `props`: The `props` argument to {parse_inline_modifiers()}.
The return value is the object created by `generate_obj`, with properties filled in describing the modifiers of the
term in question. Note that `props.outer_container` and the `overall` setting of the `props.param_mods` structure are
respected, but `props.splitchar` is ignored because the splitting happens in the caller. Specifically, if there are any
modifiers with the `overall` setting, `props.separated_groups` and `props.group_index` must be given so that the
function is able to determine if the modifier is indeed attached to the last term, and `props.outer_container` must be
given because that is where such modifiers are stored. Otherwise, none of these settings need be given.
]==]
function export.parse_inline_modifiers_from_segments(data)
local props = data.props
local group = data.group
local function get_valid_prefixes()
local valid_prefixes = {}
for param_mod, mod_props in pairs(props.param_mods) do
if not mod_props.deprecated then
insert(valid_prefixes, param_mod)
end
end
sort(valid_prefixes)
return valid_prefixes
end
local function get_arg_gloss()
if props.paramname then
return ("%s=%s"):format(props.paramname, data.arg)
else
return data.arg
end
end
local parse_err = props.parse_err or export.make_parse_err(get_arg_gloss())
local term_obj = props.generate_obj(group[1], parse_err)
for k = 2, #group - 1, 2 do
if group[k + 1] ~= "" then
parse_err("Extraneous text '" .. group[k + 1] .. "' after modifier")
end
local group_k = group[k]
if props.pre_normalize_modifiers then
-- FIXME: For some use cases, we might have to pass more information.
group_k = props.pre_normalize_modifiers {
modtext = group_k,
parse_err = parse_err
}
end
local modtext = group_k:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Modifier '" .. group_k .. "' isn't surrounded by angle brackets")
end
local prefix, val = modtext:match("^([a-zA-Z0-9+_-]+):(.*)$")
if not prefix then
local valid_prefixes = get_valid_prefixes()
for i, valid_prefix in ipairs(valid_prefixes) do
valid_prefixes[i] = "'" .. valid_prefix .. ":'"
end
parse_err(("Modifier %s%s lacks a prefix, should begin with one of %s"):format(
group_k, group_k ~= group[k] and (" (normalized from %s)"):format(group[k]) or "",
list_to_text(valid_prefixes)))
end
local prefix_parse_err
if props.parse_err then
prefix_parse_err = function(msg, stack_frames_to_ignore)
props.parse_err(("%s: modifier prefix '%s' in %s"):format(msg, prefix, group[k]),
stack_frames_to_ignore)
end
else
prefix_parse_err = export.make_parse_err(("modifier prefix '%s' in %s in %s"):format(
prefix, group[k], get_arg_gloss()))
end
if props.param_mods[prefix] then
local mod_props = props.param_mods[prefix]
if mod_props.replaced_by == false then
prefix_parse_err(
("Prefix has been removed and is no longer valid%s%s"):format(
mod_props.reason and ", " .. mod_props.reason or "",
mod_props.instead and "; instead, " .. mod_props.instead or "")
)
elseif mod_props.replaced_by then
prefix_parse_err(
("Prefix has been replaced by '%s'%s"):format(
mod_props.replaced_by, mod_props.reason and ", " .. mod_props.reason or "")
)
end
local key = mod_props.item_dest or prefix
local dest
if mod_props.overall then
if not data.separated_groups then
prefix_parse_err("Internal error: `data.separated_groups` not given when `overall` is seen")
end
if not props.outer_container then
-- This should have been caught earlier during validation in parse_inline_modifiers().
prefix_parse_err("Internal error: `props.outer_container` not given when `overall` is seen")
end
if data.group_index ~= #data.separated_groups then
prefix_parse_err("Prefix should occur after the last comma-separated term")
end
dest = props.outer_container
else
dest = term_obj
end
local converted = val
if mod_props.type or mod_props.set or mod_props.sublist or mod_props.convert then
-- WARNING: Here as an optimization we embed some knowledge of convert_val() in [[Module:parameters]],
-- specifically that if none of `type`, `set`, `sublist` and `convert` are set, the conversion is an
-- identity operation and can be skipped. (convert_val() also makes use of the fields `method` and
-- `family`, but only if `type` is set to certain values such as "language", "family" or "script", and
-- makes use of the field `required`, but only if `set` is set.) If this becomes problematic, consider
-- removing the optimization.
converted = convert_val(converted, prefix_parse_err, mod_props)
end
local store = props.param_mods[prefix].store
if not store then
if dest[key] then
prefix_parse_err("Prefix occurs twice")
end
dest[key] = converted
elseif store == "insert" then
if not dest[key] then
dest[key] = {converted}
else
insert(dest[key], converted)
end
elseif store == "insertIfNot" then
if not dest[key] then
dest[key] = {converted}
else
insert_if_not(dest[key], converted)
end
elseif store == "insert-flattened" then
if not dest[key] then
dest[key] = converted
else
for _, obj in ipairs(converted) do
insert(dest[key], obj)
end
end
elseif store == "insertIfNot-flattened" then
if not dest[key] then
dest[key] = converted
else
for _, obj in ipairs(converted) do
insert_if_not(dest[key], obj)
end
end
elseif type(store) == "string" then
prefix_parse_err(("Internal caller error: Unrecognized value '%s' for `store` property"):format(store))
elseif not is_callable(store) then
prefix_parse_err(("Internal caller error: Unrecognized type for `store` property %s"):format(dump(store)))
else
store{
dest = dest,
key = key,
converted = converted,
raw = val,
parse_err = prefix_parse_err
}
end
else
local valid_prefixes = get_valid_prefixes()
for i, valid_prefix in ipairs(valid_prefixes) do
valid_prefixes[i] = "'" .. valid_prefix .. "'"
end
prefix_parse_err("Unrecognized prefix, should be one of " ..
list_to_text(valid_prefixes))
end
end
return term_obj
end
return export
rpyhwrvd7abocvc6jbx3039wwqhfnqz
Mòideal:table/insertIfNot
828
16710
86198
2026-07-19T23:41:30Z
Altronic
4137
Copy from English Wiktionary
86198
Scribunto
text/plain
local table_find_module = "Module:table/find"
local insert = table.insert
local type = type
local function table_find(...)
table_find = require(table_find_module)
return table_find(...)
end
--[==[
Given a `list` and a `new_item` to be inserted, append the value to the end of the list if not already present (or insert at an arbitrary position, if `options.pos` is given; see below). Comparison is by value, using {deepEquals}.
`options` is an optional table of additional options to control the behavior of the operation. The following options are recognized:
* `pos`: Position at which insertion happens (i.e. before the existing item at position `pos`).
* `comparison`: Function of two arguments to compare whether `item` is equal to an existing item in `list`. If unspecified, items are considered equal if either the standard equality operator {==} or {deepEquals} return {true}. As a special case, if the string value {"=="} is specified, then the standard equality operator alone will be used.
* `key`: Function of one argument to return a comparison key, which will be used with the comparison function. The key function is applied to both `item` and the existing item in `list` to compare against, and the comparison is done against the results. This is useful when inserting a complex structure into an existing list while avoiding duplicates.
* `combine`: Function of three arguments (the existing item, the new item and the position, respectively) to combine an existing item with `new_item`, when `new_item` is found in `list`. If unspecified, the existing item is left alone.
Returns {false} if an entry is already found, or {true} if inserted. By default, {false} indicates that no change was made to the input table, but if the `combine` is used, {false} indicates that the pre-existing entry was modified.
For compatibility, `pos` can be specified directly as the third argument in place of `options`, but this is not recommended for new code.
NOTE: This function is O(N) in the size of the existing list. If you use this function in a loop to insert several items, you will get O(M*(M+N)) behavior, effectively O((M+N)^2). Thus it is not recommended to use this unless you are sure the total number of items will be small. (An alternative for large lists is to insert all the items without checking for duplicates, and use {removeDuplicates()} at the end.)]==]
return function(list, new_item, options)
local pos
if options then
if type(options) == "number" then
pos, options = options, nil
else
pos = options.pos
end
end
local i = table_find(list, new_item, options)
if not i then
if pos == nil then
insert(list, new_item)
else
insert(list, pos, new_item)
end
return true
elseif options ~= nil then
local combine_func = options.combine
if combine_func ~= nil then
local newval = combine_func(list[i], new_item, i)
if newval ~= nil then
list[i] = newval
end
end
end
return false
end
tjy2hflim20bhcfnmmalkf8958c64ib
Teamplaid:iolra de
10
16711
86199
2026-07-19T23:42:08Z
Altronic
4137
Copy from English Wiktionary
86199
wikitext
text/x-wiki
{{#invoke:form of/templates|tagged_form_of_t|p}}<noinclude>{{documentation}}</noinclude>
qmyzk2jvm2iofe7xf4ahwds3h8uqqor
Mòideal:table/find
828
16712
86200
2026-07-19T23:43:11Z
Altronic
4137
Copy from English Wiktionary
86200
Scribunto
text/plain
local table_deep_equals_module = "Module:table/deepEquals"
local deep_equals
local function get_deep_equals()
deep_equals, get_deep_equals = require(table_deep_equals_module), nil
return deep_equals
end
local function equality_op(a, b)
return a == b
end
--[==[
Given a list and a value to be found, returns the value's index if the value is in the array portion of the list, or {nil} if not found.
`options` is an optional table of additional options to control the behavior of the operation. The following options are recognized:
* `comparison`: Function of two arguments to compare whether `item` will be deemed equal to an existing item in `list`. If unspecified, items are deemed equal if {deepEquals} returns {true} (which is always the case if the equality operator {==} would return true). As a special case, if the string value {"=="} is specified, then the equality operator alone will be used.
* `key`: Function of one argument to return a comparison key, which will be used with the comparison function. The key function is applied to both `item` and the existing item in `list` to compare against, and the comparison is done against the results.]==]
return function(list, x, options)
local i, key_func, comp_func = 1
if options == nil then
comp_func = deep_equals or get_deep_equals()
else
key_func, comp_func = options.key, options.comparison
if key_func ~= nil then
x = key_func(x)
end
if comp_func == nil then
comp_func = deep_equals or get_deep_equals()
elseif comp_func == "==" then
comp_func = equality_op
end
end
while true do
local v = list[i]
if v == nil then
return nil
elseif key_func ~= nil then
v = key_func(v)
end
if comp_func(v, x) then
return i
end
i = i + 1
end
end
rwdwjaudop2hyb464ozqxzbdhpka0td
Mòideal:table/deepEquals
828
16713
86201
2026-07-19T23:44:01Z
Altronic
4137
Copy from English Wiktionary
86201
Scribunto
text/plain
local getmetatable = getmetatable
local next = next
local pairs = pairs
local type = type
local function is_eq(a, b, seen, include_mt)
-- If `a` and `b` have been compared before, return the memoized result. This will usually be true, since failures normally fail the whole check outright, but match failures are tolerated during the laborious check without this happening, since it compares key/value pairs until it finds a match, so it could be false.
local memo_a = seen[a]
if memo_a then
local result = memo_a[b]
if result ~= nil then
return result
end
-- To avoid recursive references causing infinite loops, assume the tables currently being compared are equivalent by memoizing the comparison as true; this will be corrected to false if there's a match failure.
memo_a[b] = true
else
memo_a = {[b] = true}
seen[a] = memo_a
end
-- Don't bother checking `memo_b` for `a`, since if `a` and `b` had been compared before then `b` would be in `memo_a`, but it isn't.
local memo_b = seen[b]
if memo_b then
memo_b[a] = true
else
memo_b = {[a] = true}
seen[b] = memo_b
end
-- If `include_mt` is set, check the metatables are equivalent.
if include_mt then
local mt_a, mt_b = getmetatable(a), getmetatable(b)
if not (mt_a == mt_b or type(mt_a) == "table" and type(mt_b) == "table" and is_eq(mt_a, mt_b, seen, true)) then
memo_a[b], memo_b[a] = false, false
return false
end
end
-- Copy all key/values pairs in `b` to `remaining_b`, and count the size: this uses `pairs`, which will also be used to iterate over `a`, ensuring that `a` and `b` are iterated over using the same iterator. This is necessary to ensure that `deepEquals(a, b)` and `deepEquals(b, a)` always give the same result. Simply iterating over `a` while accessing keys in `b` for comparison would ignore any `__pairs` metamethod that `b` has, which could cause asymmetrical outputs if `__pairs` returns more or less than the complete set of key/value pairs accessible via `__index`, so using `pairs` for both `a` and `b` prevents this.
-- TODO: handle exotic `__pairs` methods which return the same key multiple times with different values.
local remaining_b, size_b = {}, 0
for k_b, v_b in pairs(b) do
remaining_b[k_b], size_b = v_b, size_b + 1
end
-- Fast check: iterate over the keys in `a`, checking if an equivalent value exists at the same key in `remaining_b`. As matches are found, key/value pairs are removed from `remaining_b`. If any keys in `a` or `remaining_b` are tables, the fast check will only work if the exact same object exists as a key in the other table. Any others from `a` that don't match anything in `remaining_b` are added to `remaining_a`, while those in `remaining_b` that weren't found will still remain once the loop ends. `remaining_a` and `remaining_b` are then compared at the end with the laborious check.
local size_a, remaining_a = 0
for k, v_a in pairs(a) do
local v_b = remaining_b[k]
-- If `k` isn't in `remaining_b`, `a` and `b` can't be equivalent unless it's a table.
if v_b == nil then
if type(k) ~= "table" then
memo_a[b], memo_b[a] = false, false
return false
-- Otherwise, add the `k`/`v_a` pair to `remaining_a` for the laborious check.
elseif not remaining_a then
remaining_a = {}
end
remaining_a[k], size_a = v_a, size_a + 1
-- Otherwise, if `k` exists in `a` and `remaining_b`, `v_a` and `v_b` must be equivalent for there to be a match.
elseif v_a == v_b or type(v_a) == "table" and type(v_b) == "table" and is_eq(v_a, v_b, seen, include_mt) then
remaining_b[k], size_b = nil, size_b - 1
else
memo_a[b], memo_b[a] = false, false
return false
end
end
-- Must be the same number of remaining keys in each table.
if size_a ~= size_b then
memo_a[b], memo_b[a] = false, false
return false
-- If the size is 0, there's nothing left to check.
elseif size_a == 0 then
return true
end
-- Laborious check: since it's not possible to use table lookups to check if two keys are equivalent when they're tables, check each key/value pair in `remaining_a` against every key/value pair in `remaining_b` until a match is found, removing the matching key/value pair from `remaining_b` each time, to ensure one-to-one equivalence.
for k_a, v_a in next, remaining_a do
local success
for k_b, v_b in next, remaining_b do
-- Keys/value pairs must be equivalent in order to match.
if ( -- More efficient to compare the values first, as they might not be tables.
(v_a == v_b or type(v_a) == "table" and type(v_b) == "table" and is_eq(v_a, v_b, seen, include_mt)) and
(k_a == k_b or type(k_a) == "table" and type(k_b) == "table" and is_eq(k_a, k_b, seen, include_mt))
) then
-- Remove matched key from `remaining_b`, and break the inner loop.
success, remaining_b[k_b] = true, nil
break
end
end
-- Fail if `remaining_b` runs out of keys, as the `k_a`/`v_a` pair still hasn't matched.
if not success then
memo_a[b], memo_b[a] = false, false
return false
end
end
-- If every key/value pair in `remaining_a` matched with one in `remaining_b`, `a` and `b` must be equivalent. Note that `remaining_b` will now be empty, since the laborious check only starts if `remaining_a` and `remaining_b` are the same size.
return true
end
--[==[
Recursively compare two values that may be tables, and returns true if all key-value pairs are structurally equivalent. Note that this handles arbitrary nesting of subtables (including recursive nesting) to any depth, for keys as well as values.
If `include_mt` is true, then metatables are also compared.]==]
return function(a, b, include_mt)
-- Do simple checks before calling is_eq to avoid generating an unnecessary memo.
-- Simple equality check and type check; if not a ~= b, a and b can only be equivalent if they're both tables.
return a == b or type(a) == "table" and type(b) == "table" and is_eq(a, b, {}, include_mt)
end
6rgswepb0gwhbiojbyrcr1at7m4h27n