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 = " +&lrm; ", 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 "&amp;#91;" to be treated equivalently, with either one closed by either "]" or "&amp;#93;". To do this, first replace "&amp;#91;" and "&amp;#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