模組:Place
外观
This module implements {{place}}. Comments can be found throughout the module explaining it. Data describing the recognized placetypes and how to categorize them can be found in Module:place/data. The recognized place names are generally found in Module:place/shared-data, which (as the name suggests) is shared by Module:place/data and Module:category tree/topic cat/data/Places (which handles the generation of page contents for place categories such as Category:en:Cities in Osaka Prefecture).
local export = {}
local force_cat = false -- set to true for testing
local m_placetypes = require("Module:place/placetypes")
local m_links = require("Module:links")
local memoize = require("Module:memoize")
local m_strutils = require("Module:string utilities")
local m_table = require("Module:table")
-- Chinese localization data (zh_name, zh_suffix, qualifiers, extra-info labels, etc.)
local m_zh_data = require("Module:place/data")
local debug_track_module = "Module:debug/track"
local en_utilities_module = "Module:en-utilities"
local form_of_module = "Module:form of"
local languages_module = "Module:languages"
local parse_interface_module = "Module:parse interface"
local parse_utilities_module = "Module:parse utilities"
local parameter_utilities_module = "Module:parameter utilities"
local utilities_module = "Module:utilities"
local zhlang = require(languages_module).getByCode("zh")
local rmatch = m_strutils.match
local rfind = m_strutils.find
local ulen = m_strutils.len
local split = m_strutils.split
local dump = mw.dumpObject
local insert = table.insert
local concat = table.concat
local pluralize = require(en_utilities_module).pluralize
local extend = m_table.extend
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local internal_error = m_placetypes.internal_error
local process_error = m_placetypes.process_error
local placetype_data = m_placetypes.placetype_data
-- Shorthand for the ZH localization string table.
local zh_strings = m_zh_data.zh_strings
--[==[
ZH fork of [[Module:place]] (enwikt). Architecture and data structures are identical to the English version;
only the display-formatting functions are changed to produce Chinese-language definitions.
Key structural differences from enwikt:
* Old-style holonyms are reversed (big→small containment order) and joined with 、/和 rather than "in X, Y".
* Sentence pattern: 位於[holonyms]的[placetype] (or [holonyms]的[placetype] for "of"-type placetypes).
* No articles (a/an/the) in generated text.
* Pref/suf holonym affixes append the Chinese zh_suffix instead of English "placetype of X".
* Translations are joined with "," (full-width comma); parenthetical glosses use ().
* Extra-info labels (capital, largest city, etc.) come from Module:place/data zh_extra_info_labels.
* sentence_style is always true for ZH (extra info uses "。" separator, not ";").
]==]
--[==[ var:
List specifying the allowed form-of directives, used for former names, official names, abbreviations, etc. of places.
The key is the form-of directive and the value is an object with the following properties:
* `text`: The actual text displayed before the terms. If the value is `+`, the key is used as the text. If the value is
a function, it is passed a single argument, the overall place spec (see comment at top of file) and should return
the text to be displayed.
* `type_prefix`: The prefix used to generate the placetype for looking up the appropriate category or categories in the
placetype data structure. Can be omitted if there are no categories associated with the directive.
* `conjunction`: The conjunction used to join multiple terms, defaulting to `and`.
* `cat`: Additional category or categories to add the term to, whenever this particular directive is used.
* `default_foreign`: If specified, the default language of terms given along with this directive is the language in
{{para|1}}; otherwise it is English.
]==]
export.all_form_of_directives = {
["former name of"] = {text = "+", type_prefix = "FORMER_NAME_OF"},
["fmr of"] = {alias_of = "former name of"},
["ancient name of"] = {text = "+", type_prefix = "FORMER_NAME_OF"},
["official name of"] = {text = "+", type_prefix = "OFFICIAL_NAME_OF"},
["former official name of"] = {text = "+", type_prefix = "FORMER_OFFICIAL_NAME_OF"},
["long form of"] = {text = "+", type_prefix = "LONG_FORM_OF"},
["former long form of"] = {text = "+", type_prefix = "FORMER_LONG_FORM_OF"},
["nickname for"] = {text = "+", type_prefix = "NICKNAME_FOR"},
["official nickname for"] = {text = "+", type_prefix = "OFFICIAL_NICKNAME_FOR"},
["former nickname for"] = {text = "+", type_prefix = "FORMER_NICKNAME_FOR"},
["derogatory name for"] = {text = "[[Appendix:Glossary#derogatory|derogatory]] name for", type_prefix = "DEROGATORY_NAME_FOR"},
["synonym of"] = {text = "+"},
["syn of"] = {alias_of = "synonym of"},
["abbreviation of"] = {text = "[[Appendix:Glossary#abbreviation|abbreviation]] of", type_prefix = "ABBREVIATION_OF", cat = "cln:abbreviations",
default_foreign = true},
["abbr of"] = {alias_of = "abbreviation of"},
["abbrev of"] = {alias_of = "abbreviation of"},
["initialism of"] = {text = "[[Appendix:Glossary#initialism|initialism]] of", type_prefix = "ABBREVIATION_OF", cat = "cln:initialisms",
default_foreign = true},
["init of"] = {alias_of = "initialism of"},
["acronym of"] = {text = "[[Appendix:Glossary#acronym|acronym]] of", type_prefix = "ABBREVIATION_OF", cat = "cln:acronyms",
default_foreign = true},
["syllabic abbreviation of"] = {text = "[[Appendix:Glossary#syllabic abbreviation|syllabic abbreviation]] of", type_prefix = "ABBREVIATION_OF", cat = "cln:syllabic abbreviations",
default_foreign = true},
["sylabbr of"] = {alias_of = "syllabic abbreviation of"},
["sylabbrev of"] = {alias_of = "syllabic abbreviation of"},
["ellipsis of"] = {text = "[[Appendix:Glossary#ellipsis|ellipsis]] of", type_prefix = "ELLIPSIS_OF", cat = "cln:ellipses",
default_foreign = true},
["ellip of"] = {alias_of = "ellipsis of"},
["clipping of"] = {text = "[[Appendix:Glossary#clipping|clipping]] of", type_prefix = "CLIPPING_OF", cat = "cln:clippings",
default_foreign = true},
["clip of"] = {alias_of = "clipping of"},
["alternative form of"] = {text = "+", default_foreign = true},
["alt form"] = {alias_of = "alternative form of"},
["alternative spelling of"] = {text = "+", default_foreign = true},
["alt spell"] = {alias_of = "alternative spelling of"},
["alt sp"] = {alias_of = "alternative spelling of"},
["dated form of"] = {text = "[[Appendix:Glossary#dated|dated]] form of", type_prefix = "DATED_FORM_OF", cat = "cln:dated forms",
default_foreign = true},
["dated form"] = {alias_of = "dated form of"},
["dated spelling of"] = {text = "[[Appendix:Glossary#dated|dated]] spelling of", type_prefix = "DATED_FORM_OF", cat = "cln:dated forms",
default_foreign = true},
["dated spell"] = {alias_of = "dated spelling of"},
["dated sp"] = {alias_of = "dated spelling of"},
["archaic form of"] = {text = "[[Appendix:Glossary#archaic|archaic]] form of", type_prefix = "ARCHAIC_FORM_OF", cat = "cln:archaic forms",
default_foreign = true},
["arch form"] = {alias_of = "archaic form of"},
["archaic spelling of"] = {text = "[[Appendix:Glossary#archaic|archaic]] spelling of", type_prefix = "ARCHAIC_FORM_OF", cat = "cln:archaic forms",
default_foreign = true},
["arch spell"] = {alias_of = "archaic spelling of"},
["arch sp"] = {alias_of = "archaic spelling of"},
["obsolete form of"] = {text = "[[Appendix:Glossary#obsolete|obsolete]] form of", type_prefix = "OBSOLETE_FORM_OF", cat = "cln:obsolete forms",
default_foreign = true},
["obs form"] = {alias_of = "obsolete form of"},
["obsolete spelling of"] = {text = "[[Appendix:Glossary#obsolete|obsolete]] spelling of", type_prefix = "OBSOLETE_FORM_OF", cat = "cln:obsolete forms",
default_foreign = true},
["obs spell"] = {alias_of = "obsolete spelling of"},
["obs sp"] = {alias_of = "obsolete spelling of"},
}
local function get_seat_text(overall_place_spec)
local placetype = overall_place_spec.descs[1].placetypes[1]
if placetype == "county" or placetype == "counties" then
return "county seat"
elseif placetype == "parish" or placetype == "parishes" then
return "parish seat"
elseif placetype == "borough" or placetype == "boroughs" then
return "borough seat"
else
return "seat"
end
end
--[==[ var:
List specifying the allowed arguments containing extra information that is sometimes added to a definition, such as the
capital, largest city, modern name, official name, etc., along with associated properties; displayed in the order given.
See enwikt [[Module:place]] for full documentation of each field.
]==]
export.extra_info_args = {
{arg = "modern", text = "+", conjunction = "or", display_even_when_dropped = true},
{arg = "now", text = "now,", conjunction = "or", display_even_when_dropped = true},
{arg = "full", text = "in full,", conjunction = "or", display_even_when_dropped = true},
{arg = "short", text = "short form", conjunction = "or"},
{arg = "abbr", text = "abbreviation", conjunction = "or"},
{arg = "former", text = "formerly,"},
{arg = "official", text = "official name", match_sentence_style = true, auto_plural = true, with_colon = true},
{arg = "capital", text = "+", match_sentence_style = true, auto_plural = true, with_colon = true},
{arg = "largest city", text = "+", match_sentence_style = true, auto_plural = true, with_colon = true},
{arg = "caplc", text = "capital and largest city", match_sentence_style = true, auto_plural = false,
with_colon = true},
{arg = "seat", text = get_seat_text, match_sentence_style = true, auto_plural = true, with_colon = true},
{arg = "shire town", text = "+", match_sentence_style = true, auto_plural = true, with_colon = true},
{arg = "headquarters", text = "+", match_sentence_style = true, auto_plural = false, with_colon = true},
{arg = "center", text = "administrative center", match_sentence_style = true, auto_plural = false, with_colon = true},
{arg = "centre", text = "administrative centre", match_sentence_style = true, auto_plural = false, with_colon = true},
}
export.extra_info_arg_map = {}
for _, spec in ipairs(export.extra_info_args) do
export.extra_info_arg_map[spec.arg] = spec
end
----------- Wikicode utility functions
-- Return a wikilink link {{l|language|text}}
local function link(text, langcode, id)
if not langcode then
return text
end
return m_links.language_link( -- l10n, avoid simp/trad or applying special fonts
{
term = text,
alt = text,
lang = require(languages_module).getByCode(langcode, true, "allow etym"),
id = id
}
)
end
---------- Basic utility functions
-- Add the page to a tracking "category". To see the pages in the "category",
-- go to [[Wiktionary:Tracking/place/PAGE]] and click on "What links here".
local function track(page)
require(debug_track_module)("place/" .. page)
return true
end
local function ucfirst_all(text)
if text:find(" ") then
local parts = split(text, " ", true)
for i, part in ipairs(parts) do
parts[i] = m_strutils.ucfirst(part)
end
return concat(parts, " ")
else
return m_strutils.ucfirst(text)
end
end
local function lc(text)
return mw.getContentLanguage():lc(text)
end
---------- Argument parsing functions and utilities
-- Split an argument on comma, but not comma followed by whitespace.
local function split_on_comma(val)
if val:find(",") then
return require(parse_interface_module).split_on_comma(val)
else
return {val}
end
end
-- Split an argument on slash, but not slash occurring inside of HTML tags like </span> or <br />.
local function split_on_slash(arg)
if arg:find("<") then
local m_parse_utilities = require(parse_utilities_module)
-- We implement this by parsing balanced segment runs involving <...>, and splitting on slash in the remainder.
-- The result is a list of lists, so we have to rejoin the inner lists by concatenating.
local segments = m_parse_utilities.parse_balanced_segment_run(arg, "<", ">")
local slash_separated_groups = m_parse_utilities.split_alternating_runs(segments, "/")
for i, group in ipairs(slash_separated_groups) do
slash_separated_groups[i] = concat(group)
end
return slash_separated_groups
else
return split(arg, "/", true)
end
end
-- Implement "implications", i.e. where the presence of a given holonym causes additional holonym(s) to be added.
-- Implications apply only to categorization.
local function handle_category_implications(place_descriptions, implication_data)
for i, desc in ipairs(place_descriptions) do
if desc.holonyms then
local new_holonyms = {}
for _, holonym in ipairs(desc.holonyms) do
insert(new_holonyms, holonym)
local imp_data = m_placetypes.get_equiv_placetype_prop(holonym.placetype, function(pt)
local implication = implication_data[pt] and implication_data[pt][holonym.unlinked_placename]
if implication then
return implication
end
end)
if imp_data then
for _, holonym_to_add in ipairs(imp_data) do
local split_holonym = split_on_slash(holonym_to_add)
if #split_holonym ~= 2 then
internal_error("Invalid holonym in implications: %s", holonym_to_add)
end
local holonym_placetype, holonym_placename = unpack(split_holonym, 1, 2)
local new_holonym = {
placetype = holonym_placetype, unlinked_placename = holonym_placename
}
insert(new_holonyms, new_holonym)
m_placetypes.key_holonym_into_place_desc(desc, new_holonym)
end
end
end
desc.holonyms = new_holonyms
end
end
end
-- Split a holonym (e.g. "continent/Europe" or "country/en:Italy" or "in southern" or "r:suf/O'Higgins" or
-- "c/Austria,Germany,Czech Republic") into its components. Return a list of holonym objects (see top of file). Note
-- that if there isn't a slash in the holonym (e.g. "in southern"), the `placetype` field of the holonym will be nil.
-- Placetype aliases (e.g. "r" for "region") and placename aliases (e.g. "US" or "USA" for "United States") will be
-- expanded.
local function split_holonym(raw)
local no_display, combined_holonym = raw:match("^(!)(.*)$")
no_display = not not no_display
combined_holonym = combined_holonym or raw
local suppress_comma, combined_holonym_without_comma = combined_holonym:match("^(%*)(.*)$")
suppress_comma = not not suppress_comma
combined_holonym = combined_holonym_without_comma or combined_holonym
local holonym_parts = split_on_slash(combined_holonym)
if #holonym_parts == 1 then
-- `unlinked_placename` should not be used.
return {{display_placename = combined_holonym, no_display = no_display, suppress_comma = suppress_comma}}
end
-- Rejoin further slashes in case of slash in holonym placename, e.g. Admaston/Bromley.
local placetype = holonym_parts[1]
local placename = concat(holonym_parts, "/", 2)
-- Check for modifiers after the holonym placetype.
local split_holonym_placetype = split(placetype, ":", true)
placetype = split_holonym_placetype[1]
local affix_type
local saw_also
local saw_the
for i = 2, #split_holonym_placetype do
local modifier = split_holonym_placetype[i]
if modifier == "also" then
if saw_also then
error(("Modifier ':also' occurs twice in holonym '%s'"):format(combined_holonym))
end
saw_also = true
elseif modifier == "the" then
if saw_the then
error(("Modifier ':the' occurs twice in holonym '%s'"):format(combined_holonym))
end
saw_the = true
elseif modifier == "pref" or modifier == "Pref" or modifier == "suf" or modifier == "Suf" or
modifier == "noaff" then
if affix_type then
error(("Affix-type modifier ':%s' occurs twice in holonym '%s'"):format(modifier, combined_holonym))
end
affix_type = modifier
else
error(("Unrecognized holonym placetype modifier '%s', should be one of " ..
"'pref', 'Pref', 'suf', 'Suf', 'noaff', 'also' or 'the'"):format(modifier))
end
end
placetype = m_placetypes.resolve_placetype_aliases(placetype)
local holonyms = split_on_comma(placename)
local pluralize_affix = #holonyms > 1
local affix_holonym_index = (affix_type == "pref" or affix_type == "Pref") and 1 or affix_type == "noaff" and 0 or
#holonyms
for i, placename in ipairs(holonyms) do
-- Check for langcode before the holonym placename, but don't get tripped up by Wikipedia links, which begin
-- "[[w:...]]" or "[[wikipedia:]]".
local langcode, placename_without_langcode = rmatch(placename, "^([^%[%]]-):(.*)$")
if langcode then
placename = placename_without_langcode
end
placename = m_placetypes.resolve_placename_display_aliases(placetype, placename)
holonyms[i] = {
placetype = placetype,
display_placename = placename,
unlinked_placename = m_placetypes.remove_links_and_html(placename),
langcode = langcode,
affix_type = i == affix_holonym_index and affix_type or nil,
pluralize_affix = i == affix_holonym_index and pluralize_affix,
suppress_affix = i ~= affix_holonym_index,
no_display = no_display,
suppress_comma = suppress_comma,
continue_cat_loop = saw_also,
force_the = i == 1 and saw_the,
}
end
return holonyms
end
local get_param_mods = memoize(function()
local m_param_utils = require(parameter_utilities_module)
return m_param_utils.construct_param_mods {
{group = {"link", "q", "l", "ref"}},
{param = "eq"},
{param = "conj", set = {["and"] = true, ["or"] = true, ["and/or"] = true}, overall = true},
}
end)
local function parse_term_with_inline_modifiers(term, paramname, default_lang)
local function generate_obj(raw_term, parse_err)
local obj = require(parameter_utilities_module).generate_obj_maybe_parsing_lang_prefix {
term = raw_term,
parse_err = parse_err,
parse_lang_prefix = true,
}
obj.lang = obj.lang or default_lang
return obj
end
return require(parse_interface_module).parse_inline_modifiers(term, {
paramname = paramname,
param_mods = get_param_mods(),
generate_obj = generate_obj,
splitchar = ",",
outer_container = {},
})
end
local function parse_form_of_directive(arg, lang, form_of_overridden_args)
local form_of_directive, raw_terms = arg:match("^@([a-z -]+):(.*)$")
if not form_of_directive then
error("Misformatted @-directive: " .. dump(arg) .. " (directive name must be lowercase English letters and spaces, e.g. @official name of:比利時)")
end
if not export.all_form_of_directives[form_of_directive] then
local known_directives = {}
for k, _ in pairs(export.all_form_of_directives) do
insert(known_directives, '"' .. k .. '"')
end
table.sort(known_directives)
error(("Unrecognized form-of directive %s in @-directive %s; recognized directives are %s"):format(
dump(form_of_directive), dump(arg), concat(known_directives, ", ")))
end
local spec = export.all_form_of_directives[form_of_directive]
local canonical_directive = form_of_directive
if spec.alias_of then
canonical_directive = spec.alias_of
spec = export.all_form_of_directives[canonical_directive]
if not spec then
internal_error("Form-of directive alias %s points to %s, which is not a directive",
"@" .. form_of_directive, canonical_directive)
elseif spec.alias_of then
internal_error("Form-of directive alias %s points to %s, which is also an alias",
"@" .. form_of_directive, canonical_directive)
end
end
local default_foreign = spec.default_foreign
local directive_param = "@" .. form_of_directive
if form_of_overridden_args and form_of_overridden_args[canonical_directive] then
raw_terms = form_of_overridden_args[canonical_directive].new_value
local new_directive = form_of_overridden_args[canonical_directive].new_directive
local new_spec = export.all_form_of_directives[new_directive]
if not new_spec then
error(("Internal error: [[Module:transclude]] passed in unrecognized replacement directive '@%s'"):
format(new_directive))
end
if new_spec.alias_of then
error(("Internal error: [[Module:transclude]] passed in replacement directive alias '@%s', " ..
"should be canonical"):format(new_directive))
end
if new_directive ~= canonical_directive then
directive_param = directive_param .. (" (replaced with @%s)"):format(new_directive)
canonical_directive = new_directive
spec = new_spec
end
default_foreign = true
end
local terms = parse_term_with_inline_modifiers(raw_terms, directive_param,
default_foreign and lang or zhlang)
return {
directive = canonical_directive,
terms = terms.terms,
conj = terms.conj,
spec = spec,
}
end
-- Parse an argument containing extra information that is sometimes added to a definition, such as the capital, largest
-- city, modern name, official name, etc. `args` is the value from the parsed argument structure and can be either nil,
-- a string or a list. `spec` is the extra info spec corresponding to the type of extra info. Each value in `args` can
-- be a comma-separated list of terms with inline modifiers attached.
local function parse_extra_info_arg(args, spec, default_lang)
if not args then
return nil
end
if type(args) ~= "table" then
args = {args}
end
if not args[1] then
return nil
end
local terms = nil
local conj
for i, arg in ipairs(args) do
local this_terms = parse_term_with_inline_modifiers(arg, spec.arg .. (i == 1 and "" or i), default_lang)
local thisconj = this_terms.conj
if not conj then
conj = thisconj
elseif thisconj and conj ~= thisconj then
error(("Two different conjunctions '%s' and '%s' specified for |%s=; you only need to specify the " ..
"conjunction once"):format(conj, thisconj))
end
if not terms then
terms = this_terms.terms
else
m_table.extend(terms, this_terms.terms)
end
end
return {
spec = spec,
terms = terms,
conj = conj,
}
end
--[==[
Parse a "new-style" place description, with placetypes and holonyms surrounded by `<<...>>` amid otherwise raw text.
Return value is a place description object as documented at the top of the file. Exported for use by
[[Module:demonyms]].
]==]
function export.parse_new_style_place_desc(text, lang, form_of_directives, form_of_overridden_args)
local placetypes = {}
local segments = split(text, "<<(.-)>>")
local retval = {holonyms = {}, order = {}}
local form_of_directives_already_present = form_of_directives and not not form_of_directives[1]
for i, segment in ipairs(segments) do
if i % 2 == 1 then
insert(retval.order, {type = "raw", value = segment})
elseif segment:find("@") then
if not form_of_directives then
error(("Form-of directive '%s' not allowed in this context"):format(segment))
elseif form_of_directives_already_present then
error(("Saw form-of directive '%s' in new-style place desc followed by direct (separate-parameter) form-of directives; not allowed"):format(
segment))
elseif placetypes[1] or retval.holonyms[1] then
error(("Form-of directive '%s' must come first, before placetypes and holonyms"):format(segment))
else
local form_of_directive = parse_form_of_directive(segment, lang, form_of_overridden_args)
if not retval.order[1] or retval.order[1].type ~= "raw" or retval.order[2] then
internal_error("`retval.order` should have a single raw element: %s", retval.order)
end
form_of_directive.pretext = retval.order[1].value
retval.order[1] = nil
insert(form_of_directives, form_of_directive)
end
elseif segment:find("/") then
local holonyms = split_holonym(segment)
for j, holonym in ipairs(holonyms) do
if j > 1 then
if not holonym.no_display then
if j == #holonyms then
insert(retval.order, {type = "raw", value = zh_strings.and_conj})
else
insert(retval.order, {type = "raw", value = zh_strings.enum_sep})
end
end
holonym.needs_article = true
end
insert(retval.holonyms, holonym)
if not holonym.no_display then
insert(retval.order, {type = "holonym", value = #retval.holonyms})
end
m_placetypes.key_holonym_into_place_desc(retval, holonym)
end
else
local treat_as, display = segment:match("^(..-):(.+)$")
if treat_as then
segment = treat_as
else
display = segment
end
-- see if the placetype segment is just qualifiers
local only_qualifiers = true
local split_segments = split(segment, " ", true)
for _, split_segment in ipairs(split_segments) do
if m_placetypes.placetype_qualifiers[split_segment] == nil then
only_qualifiers = false
break
end
end
insert(placetypes, {placetype = segment, only_qualifiers = only_qualifiers})
if only_qualifiers then
insert(retval.order, {type = "qualifier", value = display})
else
insert(retval.order, {type = "placetype", value = display})
end
end
end
if not form_of_directives_already_present and form_of_directives and form_of_directives[1] then
form_of_directives[#form_of_directives].posttext = ""
end
local final_placetypes = {}
for i, placetype in ipairs(placetypes) do
if i > 1 and placetypes[i - 1].only_qualifiers then
final_placetypes[#final_placetypes] = final_placetypes[#final_placetypes] .. " " .. placetypes[i].placetype
else
insert(final_placetypes, placetypes[i].placetype)
end
end
retval.placetypes = final_placetypes
return retval
end
--[==[
Parse one or more "new-style" place descriptions, with placetypes and holonyms surrounded by `<<...>>` amid otherwise
raw text. Multiple descriptions are separated by two semicolons in a row. Return value is a list of place description
objects as documented at the top of the file.
]==]
local function parse_conjoined_new_style_place_desc(text, lang, form_of_directives, form_of_overridden_args)
local separate_specs = split(text, ";(;[^ ]*)")
local descs = {}
for i = 1, #separate_specs do
if i % 2 == 1 then
insert(descs, export.parse_new_style_place_desc(separate_specs[i], lang, form_of_directives,
form_of_overridden_args))
form_of_directives = nil
else
descs[#descs].separator = separate_specs[i]
end
end
return descs
end
--[=[
Process numeric and "extra info" arguments into an overall place spec, as described at the top of the file.
]=]
local function parse_overall_place_spec(data)
local args, from_tcl, extra_info_overridden_set, form_of_overridden_args =
data.args, data.from_tcl, data.extra_info_overridden_set, data.form_of_overridden_args
local descs = {}
local this_desc
-- Index of separate (semicolon-separated) place descriptions within `descs`.
local desc_index = 1
-- Index of separate holonyms within a place description. 0 means we've seen no holonyms and have yet to process
-- the placetypes that precede the holonyms. 1 means we've seen no holonyms but have already processed the
-- placetypes.
local holonym_index = 0
local in_place_desc = false
local form_of_directives = {}
-- ZH: use Chinese semicolon and space characters for multi-description joiners.
local function set_desc_joiner(desc, separator)
if separator == ";" then
this_desc.joiner = zh_strings.semicolon -- ";" instead of "; "
this_desc.include_following_article = true
elseif separator == ";;" then
this_desc.joiner = " "
else
local joiner = separator:sub(2)
if rfind(joiner, "^%a") then
this_desc.joiner = " " .. joiner .. " "
elseif joiner == "," then
-- ZH: ASCII comma → full-width ,(無空格)
this_desc.joiner = zh_strings.trans_sep
else
this_desc.joiner = joiner .. " "
end
end
end
for _, arg in ipairs(args[2]) do
if arg:find("^@") then
if not (desc_index == 1 and holonym_index == 0) then
error("@-directives cannot follow place descriptions")
end
local form_of_directive = parse_form_of_directive(arg, args[1], form_of_overridden_args)
if form_of_directives[1] then
form_of_directive.pretext = ", "
else
form_of_directive.pretext = ""
end
insert(form_of_directives, form_of_directive)
elseif arg == ";" or arg:find("^;[^ ]") then
if not this_desc then
error("Saw semicolon joiner without preceding place description")
end
set_desc_joiner(this_desc, arg)
desc_index = desc_index + 1
holonym_index = 0
in_place_desc = false
else
if arg:find("<<") then
if in_place_desc then
error("New-style place description must come first or following a separator (semicolon or similar), not directly following another description")
end
in_place_desc = true
local this_descs = parse_conjoined_new_style_place_desc(arg, args[1], form_of_directives,
form_of_overridden_args)
for j, desc in ipairs(this_descs) do
this_desc = desc
if holonym_index > 0 then
desc_index = desc_index + 1
holonym_index = 0
end
if j < #this_descs then
set_desc_joiner(this_desc, this_desc.separator)
end
descs[desc_index] = this_desc
last_was_new_style = true
holonym_index = #this_desc.holonyms + 1
end
else
-- Old-style arguments can directly follow a new-style argument; they become additional holonyms
-- tacked onto the end of the holonym list, and are displayed old-style except that there is no
-- prefix before the first one following the new-style argument.
in_place_desc = true
if holonym_index == 0 then
local entry_placetypes = split_on_slash(arg)
this_desc = {placetypes = entry_placetypes, holonyms = {}}
descs[desc_index] = this_desc
holonym_index = holonym_index + 1
else
local holonyms = split_holonym(arg)
for j, holonym in ipairs(holonyms) do
if j > 1 then
-- All but the first in a multi-holonym need an article. Not for the first one because e.g.
-- {{place|en|city|s/Arizona|c/United States}} should not display as "a city in Arizona, the
-- United States". The overall first holonym in the place description gets an article if
-- needed regardless of our setting here.
holonym.needs_article = true
-- Insert "and" before the last holonym.
if j == #holonyms then
this_desc.holonyms[holonym_index] = {
-- Use the no_display value from the first holonym; it should be the same for all
-- holonyms. `unlinked_placename` should not be used.
display_placename = "and", no_display = holonyms[1].no_display
}
holonym_index = holonym_index + 1
end
end
this_desc.holonyms[holonym_index] = holonym
m_placetypes.key_holonym_into_place_desc(this_desc, this_desc.holonyms[holonym_index])
holonym_index = holonym_index + 1
end
end
end
end
end
if form_of_directives[1] and not form_of_directives[#form_of_directives].posttext then
form_of_directives[#form_of_directives].posttext =
(args.def and args.def ~= "-" or not args.def and descs[1]) and ":" or ""
end
-- Tracking code (unchanged from enwikt).
for _, desc in ipairs(descs) do
for _, entry_placetype in ipairs(desc.placetypes) do
local splits = m_placetypes.split_qualifiers_from_placetype(entry_placetype, "no canon qualifiers")
for _, split in ipairs(splits) do
local prev_qualifier, this_qualifier, bare_placetype = unpack(split, 1, 3)
track("entry-placetype/" .. bare_placetype)
if this_qualifier then
track("entry-qualifier/" .. this_qualifier)
end
end
end
for _, holonym in ipairs(desc.holonyms) do
if holonym.placetype then
track("holonym-placetype/" .. holonym.placetype)
end
end
end
local extra_info = {}
for _, extra_info_spec in ipairs(export.extra_info_args) do
local extra_info_terms = parse_extra_info_arg(args[extra_info_spec.arg], extra_info_spec,
from_tcl and extra_info_overridden_set and extra_info_overridden_set[extra_info_spec.arg] and args[1] or
zhlang)
if extra_info_terms then
insert(extra_info, extra_info_terms)
end
end
return {
lang = args[1],
args = args,
directives = form_of_directives,
descs = descs,
extra_info = extra_info,
}
end
-------- Definition-generating functions
-- Return a string with the wikilinks to the ZH translations of the word.
-- ZH: joins multiple translations with "," (full-width comma) instead of ", ".
local function get_translations(transl, ids)
local ret = {}
for i, t in ipairs(transl) do
local arg_transls = split_on_comma(t)
local arg_ids = ids[i]
if arg_ids then
arg_ids = split_on_comma(arg_ids)
if #arg_transls ~= #arg_ids then
error(("Saw %s translation%s in t%s=%s but %s ID%s in tid%s=%s"):format(
#arg_transls, #arg_transls > 1 and "s" or "", i == 1 and "" or i, t, #arg_ids,
#arg_ids > 1 and "'s" or "", i == 1 and "" or i, ids[i]))
end
end
for j, arg_transl in ipairs(arg_transls) do
insert(ret, link(arg_transl, "zh", arg_ids and arg_ids[j] or nil))
end
end
return concat(ret, zh_strings.trans_sep) -- "," instead of ", "
end
-- Chinese has no articles; these stubs keep the call-sites intact.
local function get_placename_article(decorated_placename, placetypes, placename, suppress_holonym_use_the_check)
return nil
end
local function get_holonym_article(decorated_placename, place_desc, holonym_index)
return nil
end
-- Convert a holonym into display format. For ZH:
-- * No article prepended (Chinese has no articles).
-- * Both prefix and suffix affix types append the zh_suffix (e.g. "州", "省") directly to the linked name,
-- instead of the English "placetype of placename" / "placename placetype" construction.
-- * Display handlers are applied unchanged (they may add text based on the holonym's surrounding context).
local function format_holonym(place_desc, holonym_index, needs_article)
local holonym = place_desc.holonyms[holonym_index]
if holonym.no_display then
return ""
end
-- needs_article and force_the are not used in Chinese.
local output = holonym.display_placename
local placetype = holonym.placetype
local affix_type_pt_data, affix_type, affix_is_prefix, affix, prefix, suffix, no_affix_strings
local pt_equiv_for_affix_type, already_seen_affix, need_affix
local affix_pt_data, pt_equiv_for_affix
-- Implement display handlers (unchanged from enwikt).
local display_handler = m_placetypes.get_equiv_placetype_prop(placetype,
function(pt) return placetype_data[pt] and placetype_data[pt].display_handler end)
if display_handler then
output = display_handler(placetype, output)
end
if not holonym.suppress_affix then
-- Affix detection logic is unchanged from enwikt; only the rendering differs below.
affix_type_pt_data, pt_equiv_for_affix_type = m_placetypes.get_equiv_placetype_prop(placetype,
function(pt)
local cdpt = placetype_data[pt]
return cdpt and cdpt.affix_type and cdpt or nil
end
)
affix_pt_data, pt_equiv_for_affix = m_placetypes.get_equiv_placetype_prop(placetype,
function(pt)
local cdpt = placetype_data[pt]
return cdpt and (cdpt.affix_type or cdpt.affix or cdpt.prefix or cdpt.suffix) and cdpt or nil
end
)
if affix_type_pt_data then
affix_type = affix_type_pt_data.affix_type
need_affix = true
end
if affix_pt_data then
prefix = affix_pt_data.prefix or affix_pt_data.affix
suffix = affix_pt_data.suffix or affix_pt_data.affix
need_affix = true
end
no_affix_strings = affix_pt_data and affix_pt_data.no_affix_strings or
affix_type_pt_data and affix_type_pt_data.no_affix_strings
if holonym.affix_type and placetype then
affix_type = holonym.affix_type
prefix = prefix or placetype
suffix = suffix or placetype
need_affix = true
end
if need_affix then
affix_is_prefix = affix_type == "pref" or affix_type == "Pref"
if affix_is_prefix then
affix = prefix
else
affix = suffix
end
if not affix then
if not pt_equiv_for_affix_type then
internal_error("Something wrong, `pt_equiv_for_affix_type` not set processing holonym: %s",
holonym)
end
affix = pt_equiv_for_affix_type.placetype
if not affix then
internal_error("Something wrong, no affix could be located in `pt_equiv_for_affix_type` for " ..
"holonym %s: %s", holonym, pt_equiv_for_affix_type)
end
end
no_affix_strings = no_affix_strings or lc(affix)
-- ZH: skip pluralize_affix (Chinese doesn't pluralize)
end
end
-- ZH: resolve zh_suffix for the affix placetype before linking.
local zh_sfx_to_append = nil
if need_affix then
local _, zh_sfx = m_zh_data.get_zh_placetype_props(affix, m_placetypes)
if zh_sfx then
local zh_sfx_str = m_zh_data.format_zh_name(zh_sfx)
-- Check if zh_suffix already present in the unlinked output.
local unlinked = m_placetypes.remove_links_and_html(output)
if not unlinked:find(zh_sfx_str, 1, true) then
zh_sfx_to_append = zh_sfx_str
end
end
end
output = link(output, holonym.langcode or placetype and "zh" or nil)
-- ZH: for both pref and suf, append zh_suffix directly after the linked name.
-- (EN prefix construction "placetype of placename" is not used in Chinese.)
if zh_sfx_to_append then
output = output .. zh_sfx_to_append
end
-- No article for Chinese.
return output
end
-- Format holonyms for a ZH old-style place description.
-- Collects all holonyms, filters "and"/"or" connectors, reverses for big→small containment order,
-- groups consecutive same-type holonyms (peer locations), then joins with ZH enumeration (、/和).
-- Returns the concatenated holonym string, e.g. "美國賓夕法尼亞州", "波蘭、白俄羅斯和烏克蘭".
local function format_holonyms_zh(place_desc)
local holonyms = place_desc.holonyms
if not holonyms or #holonyms == 0 then
return ""
end
-- Collect non-"and"/"or"/"的" holonym items with their original indices.
-- A raw "的" holonym (from |的| in the template, like enwikt's |of|) signals possessive preposition;
-- filter it from display since format_old_style_place_desc_for_display already appends de_particle.
local items = {}
local has_de_joiner = false
for i, h in ipairs(holonyms) do
if not h.no_display then
local dname = h.display_placename
if dname ~= "and" and dname ~= "or" then
if not h.placetype and dname == zh_strings.de_particle then
has_de_joiner = true
else
insert(items, {holonym_index = i, placetype = h.placetype})
end
end
end
end
-- Separate trailing raw text (items after the last structural holonym) from non-trailing.
-- Trailing raw text should NOT be reversed; it is appended after the full description with ,.
local last_structural_idx = 0
for i = #items, 1, -1 do
if items[i].placetype then
last_structural_idx = i
break
end
end
local non_trailing = {}
local trailing_texts = {}
for i = 1, #items do
if i <= last_structural_idx then
insert(non_trailing, items[i])
else
insert(trailing_texts, holonyms[items[i].holonym_index].display_placename)
end
end
-- Reverse non-trailing for big→small order.
local reversed = {}
for i = #non_trailing, 1, -1 do
insert(reversed, non_trailing[i])
end
-- Group consecutive items with the same placetype (peers like multiple countries).
-- Raw-text holonyms (no placetype) break any current structural group.
local groups = {}
local i = 1
while i <= #reversed do
local item = reversed[i]
if not item.placetype then
-- Raw text (e.g. "南部", "東北部") forms its own singleton group.
local h = holonyms[item.holonym_index]
insert(groups, {type = "raw", text = h.display_placename})
i = i + 1
else
-- Structural holonym: collect consecutive ones of the same placetype.
local group = {type = "structural", items = {item}}
local pt = item.placetype
i = i + 1
while i <= #reversed do
local next_item = reversed[i]
if next_item.placetype == pt then
insert(group.items, next_item)
i = i + 1
else
break
end
end
insert(groups, group)
end
end
-- Format each group and assemble.
local parts = {}
for _, group in ipairs(groups) do
if group.type == "raw" then
insert(parts, group.text)
else
local formatted = {}
for _, item in ipairs(group.items) do
insert(formatted, format_holonym(place_desc, item.holonym_index, false))
end
if #formatted == 1 then
insert(parts, formatted[1])
elseif #formatted == 2 then
insert(parts, formatted[1] .. zh_strings.and_conj .. formatted[2])
else
local last = table.remove(formatted)
insert(parts, concat(formatted, zh_strings.enum_sep) .. zh_strings.and_conj .. last)
end
end
end
return concat(parts), has_de_joiner, trailing_texts
end
-- Get the Chinese display name for a single (possibly qualified) placetype.
-- Strips qualifiers, looks up zh_name via m_zh_data, and prepends translated qualifier text.
-- `first_holonym_placetype` enables context-sensitive names (e.g. capital city → 首都/省會/首府).
local function get_zh_placetype_name(placetype, first_holonym_placetype)
local resolved_pt = m_placetypes.resolve_placetype_aliases(placetype)
local zh_name, _, zh_by_holonym = m_zh_data.get_zh_placetype_props(resolved_pt, m_placetypes)
if zh_name then
local resolved = m_zh_data.resolve_zh_name(zh_name, zh_by_holonym, first_holonym_placetype)
return m_zh_data.format_zh_name(resolved)
end
-- Fallback to English name; add tracking for missing zh_data entries.
track("missing-zh-placetype/" .. tostring(placetype))
return placetype
end
-- Format a single entry placetype (possibly with qualifiers) for ZH display.
-- Strips qualifiers from the placetype, translates each qualifier via zh_qualifiers,
-- and prepends them to the Chinese placetype name (with wikilink markup applied).
local function format_one_zh_placetype(pt, first_holonym_placetype)
-- Try progressive qualifier splits; use the first that has a zh_name entry.
local splits = m_placetypes.split_qualifiers_from_placetype(pt)
local qualifier_prefix = ""
for _, sp in ipairs(splits) do
local prev_qualifier, this_qualifier, bare_pt = unpack(sp, 1, 3)
-- Accumulate qualifier strings.
local q_str = ""
if prev_qualifier then
q_str = (m_zh_data.zh_qualifiers[prev_qualifier] or prev_qualifier) ..
(m_zh_data.zh_qualifiers[this_qualifier] or (this_qualifier or ""))
elseif this_qualifier then
q_str = m_zh_data.zh_qualifiers[this_qualifier] or this_qualifier
end
local zh_n = m_placetypes.get_zh_placetype_display_form(bare_pt, first_holonym_placetype)
if zh_n then
-- Found a known ZH entry; return qualifier + linked display form.
return q_str .. zh_n
end
qualifier_prefix = q_str
end
-- Fallback: translate the full (possibly unrecognized) placetype; link if known.
return qualifier_prefix .. (m_placetypes.get_zh_placetype_display_form(pt, first_holonym_placetype)
or get_zh_placetype_name(pt, first_holonym_placetype))
end
-- Build the ZH entry placetype description from the `placetypes` list.
-- Handles "and"/"or" splitting (e.g. "城市/and/市鎮" → "城市和市鎮"),
-- multiple placetypes without and/or (joined with "、...和"),
-- and context-sensitive names via first_holonym_placetype.
local function get_zh_entry_placetype_desc(placetypes, first_holonym_placetype)
local is_ignorable = m_placetypes.placetype_is_ignorable
-- Find last and/or position.
local and_or_pos
for i, pt in ipairs(placetypes) do
if pt == "and" or pt == "or" then
and_or_pos = i
end
end
if and_or_pos then
-- Split into group before and group after the conjunction.
local group1, group2 = {}, {}
for i = 1, and_or_pos - 1 do
if not is_ignorable(placetypes[i]) then
insert(group1, format_one_zh_placetype(placetypes[i], first_holonym_placetype))
end
end
for i = and_or_pos + 1, #placetypes do
if not is_ignorable(placetypes[i]) then
insert(group2, format_one_zh_placetype(placetypes[i], first_holonym_placetype))
end
end
local conj_str = placetypes[and_or_pos] == "or" and "或" or zh_strings.and_conj
local all = {}
for _, v in ipairs(group1) do insert(all, v) end
for _, v in ipairs(group2) do insert(all, v) end
if #all == 0 then return "" end
if #all == 1 then return all[1] end
local last = table.remove(all)
return concat(all, zh_strings.enum_sep) .. conj_str .. last
else
-- No explicit and/or: collect all non-ignorable placetypes.
local formatted = {}
for _, pt in ipairs(placetypes) do
if not is_ignorable(pt) then
insert(formatted, format_one_zh_placetype(pt, first_holonym_placetype))
end
end
if #formatted == 0 then return "" end
if #formatted == 1 then return formatted[1] end
local last = table.remove(formatted)
return concat(formatted, zh_strings.enum_sep) .. zh_strings.and_conj .. last
end
end
-- Return the ZH description of a placetype for use in new-style <<placetype>> segments.
local function get_placetype_description(placetype, first_holonym_placetype)
return format_one_zh_placetype(placetype, first_holonym_placetype)
end
-- Return the ZH description of a qualifier for use in new-style <<qualifier>> segments.
local function get_qualifier_description(qualifier)
-- Trim to just the qualifier word (split_qualifiers_from_placetype requires a trailing non-qualifier word).
local splits = m_placetypes.split_qualifiers_from_placetype(qualifier .. " foo")
local sp = splits[#splits]
local prev_qualifier, this_qualifier = unpack(sp, 1, 2)
if this_qualifier then
local q = m_zh_data.zh_qualifiers[this_qualifier] or this_qualifier
if prev_qualifier then
return (m_zh_data.zh_qualifiers[prev_qualifier] or prev_qualifier) .. q
end
return q
end
return qualifier
end
-- Format a set of form-of directive terms (unchanged from enwikt, only conjunction uses zh_strings).
local function format_form_of_directive(overall_place_spec, directive_terms, ucfirst, from_tcl)
local formatted_terms = {}
local placetypes
if not overall_place_spec.descs[2] then
placetypes = overall_place_spec.descs[1].placetypes
else
placetypes = {}
for _, desc in ipairs(overall_place_spec.descs) do
m_table.extend(placetypes, desc.placetypes)
end
end
for _, termobj in ipairs(directive_terms.terms) do
local placename_article
if not termobj.alt and termobj.term and not termobj.term:find("%[%[") then
placename_article = get_placename_article(termobj.term, placetypes)
end
local linked_term = m_links.full_link(termobj, "term", nil, "show qualifiers")
linked_term = "<span class='form-of-definition-link'>" .. linked_term .. "</span>"
if termobj.eq then
linked_term = linked_term .. " (= " .. m_links.full_link {term = termobj.eq, lang = zhlang} .. ")"
end
if placename_article then
linked_term = placename_article .. " " .. linked_term
end
insert(formatted_terms, linked_term)
end
local spec = directive_terms.spec
local text = spec.text
if type(text) == "function" then
text = text(overall_place_spec)
end
if text == "+" then
text = directive_terms.directive
end
-- Look up Chinese translation; all non-"+" directives also get translated.
local zh_text = m_zh_data.zh_form_of_directive_labels[directive_terms.directive]
if zh_text then
text = zh_text
end
if ucfirst then
text = m_strutils.ucfirst(text)
end
if not from_tcl then
local tracking_prefix = "form-of/" .. directive_terms.directive
track(tracking_prefix)
local langcode = overall_place_spec.lang:getCode()
local full_langcode = overall_place_spec.lang:getFullCode()
track(tracking_prefix .. "/" .. langcode)
if full_langcode ~= langcode then
track(tracking_prefix .. "/" .. full_langcode)
end
if full_langcode ~= "zh" then
track(tracking_prefix .. "/non-chinese")
end
end
return (require(form_of_module).format_form_of {
text = text,
lemmas = m_table.serialCommaJoin(formatted_terms, {conj = directive_terms.conj or spec.conjunction or zh_strings.and_conj}),
lemma_classes = false,
})
end
-- Format extra-info terms (capital, largest city, modern name, etc.) for ZH.
-- * Labels come from m_zh_data.zh_extra_info_labels; capital/caplc use zh_capital_label_by_holonym_type,
-- resolved first by entry placetype (國家→首都, 省→省會…), then by holonym placetype as fallback.
-- * Separator: "。" + label in sentence style; ";" + label otherwise.
-- * Multiple terms joined with 、/和 instead of serial comma.
-- * auto_plural is skipped (Chinese doesn't pluralize).
local function format_extra_info(overall_place_spec, extra_info_terms, sentence_style)
local formatted_terms = {}
for _, termobj in ipairs(extra_info_terms.terms) do
insert(formatted_terms, m_links.full_link(termobj, nil, nil, "show qualifiers"))
end
local spec = extra_info_terms.spec
-- Resolve ZH label: look up by spec.arg, then spec.text as fallback.
local zh_label = m_zh_data.zh_extra_info_labels[spec.arg]
if not zh_label then
-- Try spec.text as the lookup key (e.g. "capital and largest city").
local spec_text = spec.text
if type(spec_text) == "function" then
spec_text = spec_text(overall_place_spec)
end
zh_label = m_zh_data.zh_extra_info_labels[spec_text] or spec_text or spec.arg
end
-- Context-sensitive capital label (首都/省會/首府/縣治…) for `capital=` and `caplc=`.
-- Priority: entry placetype (alias-resolved) → holonym placetype → keep existing zh_label.
-- For caplc, appends "和最大城市" to the resolved capital label.
if spec.arg == "capital" or spec.arg == "caplc" then
local cap_label = nil
local desc1 = overall_place_spec.descs and overall_place_spec.descs[1]
-- 1. Entry placetype (e.g. 國家→country→首都, 省→province→省會)
if desc1 then
for _, raw_pt in ipairs(desc1.placetypes) do
if not m_placetypes.placetype_is_ignorable(raw_pt) and raw_pt ~= "and" and raw_pt ~= "or" then
cap_label = m_placetypes.get_equiv_placetype_prop(raw_pt, function(pt)
return m_zh_data.zh_capital_label_by_holonym_type[pt]
end)
if cap_label then break end
end
end
end
-- 2. Holonym placetype fallback (previous behaviour)
if not cap_label and desc1 and desc1.holonyms then
for _, h in ipairs(desc1.holonyms) do
if h.placetype then
cap_label = m_zh_data.zh_capital_label_by_holonym_type[h.placetype]
if cap_label then break end
end
end
end
if cap_label then
zh_label = spec.arg == "caplc" and (cap_label .. "和最大城市") or cap_label
end
end
local text = zh_label
if spec.with_colon then
text = text .. ":" -- full-width colon
end
if sentence_style and spec.match_sentence_style then
text = "。" .. text
else
text = zh_strings.semicolon .. text
end
-- Join terms with ZH enumeration.
local conj = extra_info_terms.conj or zh_strings.and_conj
local joined
if #formatted_terms == 0 then
joined = ""
elseif #formatted_terms == 1 then
joined = formatted_terms[1]
elseif #formatted_terms == 2 then
joined = formatted_terms[1] .. conj .. formatted_terms[2]
else
local last = table.remove(formatted_terms)
joined = concat(formatted_terms, zh_strings.enum_sep) .. conj .. last
end
return text .. joined
end
-- Format an old-style place description for ZH display.
-- Chinese pattern: [位於|""][holonym_string][的][zh_placetype_name]
-- * Holonyms are reversed (big→small), grouped, and joined with 、/和.
-- * "位於" is prepended for "in"-type placetypes; omitted for "of"-type (e.g. capitals).
-- * args.a (article override) is used as a prefix to the placetype name.
local function format_old_style_place_desc_for_display(args, place_desc, desc_index, with_article, ucfirst_flag)
local placetypes = place_desc.placetypes
-- Determine the entry placetype (last non-ignorable, non-conjunction item) for preposition lookup.
local entry_placetype_for_prep = nil
for i = #placetypes, 1, -1 do
local pt = placetypes[i]
if not m_placetypes.placetype_is_ignorable(pt) and pt ~= "and" and pt ~= "or" then
entry_placetype_for_prep = pt
break
end
end
-- Determine first structured holonym placetype for context-sensitive zh_name lookups.
local first_holonym_placetype = nil
if place_desc.holonyms then
for _, h in ipairs(place_desc.holonyms) do
if h.placetype then
first_holonym_placetype = h.placetype
break
end
end
end
-- Build the Chinese placetype description (possibly with qualifiers and multiple types).
local zh_pt_name = get_zh_entry_placetype_desc(placetypes, first_holonym_placetype)
-- Handle args.a (article override): used as a direct prefix in Chinese, e.g. a=盎格魯撒克遜 → "盎格魯撒克遜王國".
if with_article and desc_index == 1 and args.a then
zh_pt_name = args.a .. zh_pt_name
end
-- Build holonym string (reversed, grouped, ZH-joined).
-- has_de_joiner is true when a bare |的| token was in the holonym list (like enwikt's |of|).
local holonym_str, has_de_joiner, trailing_texts = "", false, {}
if place_desc.holonyms and #place_desc.holonyms > 0 then
holonym_str, has_de_joiner, trailing_texts = format_holonyms_zh(place_desc)
end
-- Determine preposition style ("in" → 位於…的; "of" → …的 with no 位於).
-- An explicit |的| joiner in the template overrides to possessive (no 位於).
local use_wei_yu = true
if has_de_joiner then
use_wei_yu = false
elseif entry_placetype_for_prep then
local prep = m_placetypes.get_placetype_entry_preposition(entry_placetype_for_prep)
if prep == "of" then
use_wei_yu = false
end
end
-- Assemble final string.
local result
if holonym_str ~= "" then
if use_wei_yu then
result = zh_strings.in_preposition .. holonym_str .. zh_strings.de_particle .. zh_pt_name
else
result = holonym_str .. zh_strings.de_particle .. zh_pt_name
end
else
result = zh_pt_name
end
if #trailing_texts > 0 then
result = result .. zh_strings.trans_sep .. concat(trailing_texts, zh_strings.trans_sep)
end
return result
end
--[==[
Get the full gloss (Chinese description) of a new-style place description. New-style place descriptions are
specified with a single string containing raw text interspersed with placetypes and holonyms surrounded by `<<...>>`.
Exported for use by [[Module:demonyms]].
]==]
function export.format_new_style_place_desc_for_display(args, place_desc, with_article)
local parts = {}
local function ins(txt)
insert(parts, txt)
end
-- args.a not used in Chinese (no article system).
local first_holonym_placetype = nil
if place_desc.holonyms then
for _, h in ipairs(place_desc.holonyms) do
if h.placetype then
first_holonym_placetype = h.placetype
break
end
end
end
local max_holonym = 0
for _, order in ipairs(place_desc.order) do
local segment_type, segment = order.type, order.value
if segment_type == "raw" then
ins(segment)
elseif segment_type == "placetype" then
ins(get_placetype_description(segment, first_holonym_placetype))
elseif segment_type == "qualifier" then
ins(get_qualifier_description(segment))
elseif segment_type == "holonym" then
ins(format_holonym(place_desc, segment, false))
if segment > max_holonym then
max_holonym = segment
end
else
internal_error("Unrecognized segment type %s", segment_type)
end
end
if place_desc.holonyms and max_holonym < #place_desc.holonyms then
local holonym_no_prefix = true
for holonym_index = max_holonym + 1, #place_desc.holonyms do
-- Append remaining holonyms with a space prefix (no "in"/"of" for new-style).
ins(" " .. format_holonym(place_desc, holonym_index, false))
holonym_no_prefix = false
end
end
return concat(parts)
end
-- Return a string with the gloss (the description of the place itself, as opposed to translations).
-- `sentence_style` controls whether extra-info uses "。" (sentence) or ";" (semicolon) separation.
local function get_display_form(data)
local overall_place_spec, ucfirst, sentence_style, drop_extra_info, extra_info_overridden_set, from_tcl =
data.overall_place_spec, data.ucfirst, data.sentence_style, data.drop_extra_info,
data.extra_info_overridden_set, data.from_tcl
local args = overall_place_spec.args
local parts = {}
local function ins(txt)
table.insert(parts, txt)
end
if overall_place_spec.directives and overall_place_spec.directives[1] then
for i, directive_terms in ipairs(overall_place_spec.directives) do
ins(directive_terms.pretext)
if directive_terms.pretext ~= "" then
ucfirst = false
end
if not args.def or args.def == "-" then
ins(format_form_of_directive(overall_place_spec, directive_terms, ucfirst, from_tcl))
ucfirst = false
if i == #overall_place_spec.directives and directive_terms.posttext then
ins(directive_terms.posttext)
end
end
end
end
if args.def == "-" then
return concat(parts)
end
if args.def then
if args.def:find("<<") then
local def_desc = export.parse_new_style_place_desc(args.def, args[1])
ins(export.format_new_style_place_desc_for_display({}, def_desc, false))
else
ins(args.def)
end
else
local include_article = true
for n, desc in ipairs(overall_place_spec.descs) do
if desc.order then
ins(export.format_new_style_place_desc_for_display(args, desc, n == 1))
else
ins(format_old_style_place_desc_for_display(args, desc, n, include_article, ucfirst))
end
if desc.joiner then
ins(desc.joiner)
end
include_article = desc.include_following_article
ucfirst = false
end
end
local addl = args.addl
if addl then
if addl:find("^[;:]") then
ins(addl)
elseif addl:find("^_") then
ins(" " .. addl:sub(2))
else
ins("," .. addl)
end
end
for _, extra_info_terms in ipairs(overall_place_spec.extra_info) do
if not drop_extra_info or extra_info_terms.spec.display_even_when_dropped or
extra_info_overridden_set and extra_info_overridden_set[extra_info_terms.spec.arg] then
ins(format_extra_info(overall_place_spec, extra_info_terms, sentence_style))
end
end
return concat(parts)
end
-- Return the definition line.
-- ZH: sentence_style is always true; ucfirst is always false (no case in Chinese);
-- parentheses around glosses are full-width ().
local function get_def(data)
local overall_place_spec, from_tcl, drop_extra_info, extra_info_overridden_set, translation_follows =
data.overall_place_spec, data.from_tcl, data.drop_extra_info, data.extra_info_overridden_set,
data.translation_follows
local args = overall_place_spec.args
local sentence_style = true -- always sentence style in Chinese
local ucfirst = false -- Chinese has no case
if #args.t > 0 then
local gloss = get_display_form {
overall_place_spec = overall_place_spec,
ucfirst = false,
sentence_style = false,
drop_extra_info = drop_extra_info,
extra_info_overridden_set = extra_info_overridden_set,
from_tcl = from_tcl,
}
if from_tcl and not args.tcl_nolc then
gloss = m_strutils.lcfirst(gloss)
end
if translation_follows then
return (gloss == "" and "" or gloss .. ":") .. get_translations(args.t, args.tid)
else
return get_translations(args.t, args.tid) ..
(gloss == "" and "" or zh_strings.paren_open .. gloss .. zh_strings.paren_close)
end
else
return get_display_form {
overall_place_spec = overall_place_spec,
ucfirst = ucfirst,
sentence_style = sentence_style,
drop_extra_info = drop_extra_info,
extra_info_overridden_set = extra_info_overridden_set,
from_tcl = from_tcl,
}
end
end
---------- Functions for the category wikicode
-- The code in this section finds the categories to which a given place belongs. See enwikt [[Module:place]]
-- for detailed documentation of the algorithm.
--[=[
Find the appropriate category specs for a given place description and placetype.
See enwikt [[Module:place]] for detailed documentation.
]=]
local function find_placetype_cat_specs(data)
local entry_placetype, place_desc, first_holonym_index, overriding_holonym, from_demonym =
data.entry_placetype, data.place_desc, data.first_holonym_index, data.overriding_holonym, data.from_demonym
local form_of_directive = data.form_of_directive
local function fetch_cat_specs(holonym_to_match, index, no_fallback)
local holonym_placetype = holonym_to_match.placetype
if not holonym_placetype then
return nil
end
local holonym_placename = holonym_to_match.unlinked_placename
if not holonym_placename then
internal_error("Missing unlinked_placename in holonym (index %s): %s", index, holonym_to_match)
end
local cat_specs, equiv_entry_placetype_and_qualifier = m_placetypes.get_equiv_placetype_prop(entry_placetype,
function(equiv_entry_pt)
return m_placetypes.get_equiv_placetype_prop(holonym_placetype,
function(equiv_holonym_pt) return m_placetypes.political_division_cat_handler {
entry_placetype = equiv_entry_pt,
holonym_placetype = equiv_holonym_pt,
holonym_placename = holonym_placename,
holonym_index = index,
place_desc = place_desc,
from_demonym = from_demonym,
} end)
end,
{no_fallback = no_fallback, form_of_directive = form_of_directive}
)
if cat_specs and cat_specs[1] then
return cat_specs, equiv_entry_placetype_and_qualifier.placetype
end
local cat_handler, equiv_entry_placetype_and_qualifier = m_placetypes.get_equiv_placetype_prop(entry_placetype,
function(equiv_entry_pt)
local entry_placetype_data = m_placetypes.placetype_data[equiv_entry_pt]
if entry_placetype_data and entry_placetype_data.cat_handler then
return entry_placetype_data.cat_handler
end
end,
{no_fallback = no_fallback, form_of_directive = form_of_directive}
)
if cat_handler then
local cat_specs = m_placetypes.get_equiv_placetype_prop(holonym_placetype,
function(equiv_holonym_pt) return cat_handler {
entry_placetype = equiv_entry_placetype_and_qualifier.placetype,
holonym_placetype = equiv_holonym_pt,
holonym_placename = holonym_placename,
holonym_index = index,
place_desc = place_desc,
from_demonym = from_demonym,
} end)
if cat_specs and cat_specs[1] then
return cat_specs, equiv_entry_placetype_and_qualifier.placetype
end
end
if not no_fallback then
local cat_specs, equiv_entry_placetype_and_qualifier = m_placetypes.get_equiv_placetype_prop(entry_placetype,
function(equiv_entry_pt)
local entry_placetype_data = m_placetypes.placetype_data[equiv_entry_pt]
if entry_placetype_data then
return m_placetypes.get_equiv_placetype_prop(holonym_placetype,
function(equiv_holonym_pt)
return entry_placetype_data[equiv_holonym_pt .. "/*"]
end)
end
end,
{form_of_directive = form_of_directive}
)
if cat_specs and cat_specs[1] then
return cat_specs, equiv_entry_placetype_and_qualifier.placetype
end
end
return nil
end
if overriding_holonym then
local cat_specs, fetched_entry_placetype = fetch_cat_specs(overriding_holonym, nil)
if cat_specs and cat_specs[1] then
return {
entry_placetype = fetched_entry_placetype,
cat_specs = cat_specs,
triggering_holonym = overriding_holonym,
}
end
else
-- First pass: no fallback (exact entry placetype match only).
for i, holonym in ipairs(place_desc.holonyms) do
if first_holonym_index and i < first_holonym_index then
-- continue
else
local cat_specs, fetched_entry_placetype = fetch_cat_specs(holonym, i, "no_fallback")
if cat_specs and cat_specs[1] then
return {
entry_placetype = fetched_entry_placetype,
cat_specs = cat_specs,
triggering_holonym = holonym,
triggering_holonym_index = i,
}
end
end
end
-- Second pass: with fallback.
for i, holonym in ipairs(place_desc.holonyms) do
if first_holonym_index and i < first_holonym_index then
-- continue
else
local cat_specs, fetched_entry_placetype = fetch_cat_specs(holonym, i)
if cat_specs and cat_specs[1] then
return {
entry_placetype = fetched_entry_placetype,
cat_specs = cat_specs,
triggering_holonym = holonym,
triggering_holonym_index = i,
}
end
end
end
end
return nil
end
-- Turn a list of category specs (see comment at section top) into the corresponding categories
-- (minus the language code prefix).
local function cat_specs_to_categories(place_desc, cat_data)
local all_cats = {}
local cat_specs, entry_placetype, triggering_holonym, triggering_holonym_index =
cat_data.cat_specs, cat_data.entry_placetype, cat_data.triggering_holonym, cat_data.triggering_holonym_index
if triggering_holonym then
for _, cat_spec in ipairs(cat_specs) do
local cat
if cat_spec == true then
-- ZH: 1-char zh_name → "的" + name (e.g. "的省"); 2+ chars → no "的" (e.g. "城市").
local zh_raw = m_placetypes.get_zh_placetype_name(entry_placetype)
local de = zh_raw and m_placetypes.zh_cat_de(zh_raw) or zh_strings.de_particle
local zh_str = zh_raw and m_zh_data.format_zh_name(zh_raw) or entry_placetype
cat = "+++" .. de .. zh_str
else
cat = cat_spec
end
if cat:find("%+%+%+") then
local group, key, spec, container_trail = m_placetypes.find_matching_holonym_location {
holonym_placetype = triggering_holonym.placetype,
holonym_placename = triggering_holonym.unlinked_placename,
holonym_index = triggering_holonym_index,
place_desc = place_desc,
}
if group then
cat = cat:gsub("%+%+%+", m_strutils.replacement_escape(m_placetypes.get_prefixed_key(key, spec)))
insert(all_cats, cat)
else
mw.log(("Unable to insert category for cat spec '%s' because holonym '%s/%s' did not match a " ..
"known location"):format(cat, triggering_holonym.placetype, triggering_holonym.unlinked_placename))
track("cant-match-holonym-for-category-spec")
end
else
insert(all_cats, cat)
end
end
else
for _, cat_spec in ipairs(cat_specs) do
local cat
if cat_spec == true then
-- ZH: bare placetype category (e.g. "城市")
cat = get_zh_placetype_name(entry_placetype)
else
cat = cat_spec
if cat:find("%+%+%+") then
internal_error("Category %s contains +++ but there is no holonym to substitute", cat)
end
end
insert(all_cats, cat)
end
end
return all_cats
end
-- Return the categories (without initial lang code) that should be added to the entry, given the place description
-- and a particular entry placetype.
local function get_placetype_cats(place_desc, entry_placetype, from_demonym, form_of_directive)
local cats = {}
local first_holonym_index = 1
while first_holonym_index <= #place_desc.holonyms do
local cat_data = find_placetype_cat_specs {
entry_placetype = entry_placetype,
place_desc = place_desc,
first_holonym_index = first_holonym_index,
from_demonym = from_demonym,
form_of_directive = form_of_directive,
}
if not cat_data then
break
end
local triggering_holonym = cat_data.triggering_holonym
if not triggering_holonym then
internal_error("find_placetype_cat_specs should have returned a triggering holonym: %s", cat_data)
end
extend(cats, cat_specs_to_categories(place_desc, cat_data))
first_holonym_index = cat_data.triggering_holonym_index
local equiv_triggering_placetypes = m_placetypes.get_placetype_equivs(triggering_holonym.placetype,
{no_fallback = true})
for _, equiv in ipairs(equiv_triggering_placetypes) do
local other_holonyms_of_same_type = place_desc.holonyms_by_placetype[equiv.placetype]
if other_holonyms_of_same_type then
for _, other_placename_of_same_type in ipairs(other_holonyms_of_same_type) do
if other_placename_of_same_type ~= triggering_holonym.unlinked_placename then
local overriding_holonym = {
placetype = triggering_holonym.placetype,
unlinked_placename = other_placename_of_same_type,
}
local other_cat_data = find_placetype_cat_specs {
entry_placetype = entry_placetype,
place_desc = place_desc,
overriding_holonym = overriding_holonym,
from_demonym = from_demonym,
form_of_directive = form_of_directive,
}
if other_cat_data then
extend(cats, cat_specs_to_categories(place_desc, other_cat_data))
end
end
end
end
end
first_holonym_index = first_holonym_index + 1
while first_holonym_index <= #place_desc.holonyms do
if place_desc.holonyms[first_holonym_index].continue_cat_loop then
break
end
first_holonym_index = first_holonym_index + 1
end
end
if cats[1] then
return cats
end
local entry_pt_default, equiv_entry_placetype_and_qualifier =
m_placetypes.get_equiv_placetype_prop(entry_placetype, function(pt)
return m_placetypes.placetype_data[pt] and m_placetypes.placetype_data[pt].default
end,
{form_of_directive = form_of_directive})
if entry_pt_default then
return cat_specs_to_categories(place_desc, {
cat_specs = entry_pt_default,
entry_placetype = equiv_entry_placetype_and_qualifier.placetype,
})
end
return {}
end
--[==[
Iterate through each type of place and return a list of the categories that need to be added to the entry.
See enwikt [[Module:place]] for detailed documentation.
]==]
function export.get_cats(args, overall_place_spec, from_demonym)
local cats = {}
local place_descriptions = overall_place_spec.descs
handle_category_implications(place_descriptions, m_placetypes.cat_implications)
m_placetypes.augment_holonyms_with_container(place_descriptions)
if overall_place_spec.directives then
for _, directive_terms in ipairs(overall_place_spec.directives) do
local spec_cats = directive_terms.spec.cat
if spec_cats then
if type(spec_cats) == "string" then
spec_cats = {spec_cats}
end
for _, spec_cat in ipairs(spec_cats) do
insert(cats, spec_cat)
end
end
if directive_terms.spec.type_prefix then
for _, place_desc in ipairs(place_descriptions) do
for _, placetype in ipairs(place_desc.placetypes) do
if not m_placetypes.placetype_is_ignorable(placetype) then
extend(cats, get_placetype_cats(place_desc, placetype, from_demonym,
directive_terms.spec.type_prefix))
end
end
end
end
end
end
if not from_demonym then
local bare_categories = m_placetypes.get_bare_categories(args, overall_place_spec)
extend(cats, bare_categories)
end
for _, place_desc in ipairs(place_descriptions) do
if not from_demonym then
for _, placetype in ipairs(place_desc.placetypes) do
if not m_placetypes.placetype_is_ignorable(placetype) then
extend(cats, get_placetype_cats(place_desc, placetype))
end
end
end
extend(cats, get_placetype_cats(place_desc, "*", from_demonym))
end
if args.cat then
for _, cat in ipairs(args.cat) do
local split_cats = split_on_comma(cat)
extend(cats, split_cats)
end
end
return cats
end
-- Return the category wikicode for a list of categories.
-- ZH: uses `lang:getFullName() .. " " .. cat` format for regular categories (same as cln: format in enwikt),
-- matching zhwikt's convention of "漢語 XX" category names instead of "zh:XX".
local function format_cats(lang, cats, sort_key)
local full_cats = {}
local langname = lang:getFullName()
for _, cat in ipairs(cats) do
local cln_cat = cat:match("^cln:(.*)$")
if cln_cat then
insert(full_cats, langname .. " " .. cln_cat)
else
-- ZH: use langname prefix instead of langcode:cat
insert(full_cats, langname .. " " .. cat)
end
end
return require(utilities_module).format_categories(full_cats, lang, sort_key, nil,
force_cat or m_placetypes.get_force_cat())
end
----------- Main entry point
--[==[
Implementation of {{tl|place}}. Meant to be callable from another module.
See enwikt [[Module:place]] for full documentation of the `data` argument fields.
]==]
function export.format(data)
local template_args = data.template_args
local list_param = {list = true}
local boolean_param = {type = "boolean"}
local params = {
[1] = {required = true, type = "language", default = "und"},
[2] = {required = true, list = true},
["t"] = list_param,
["tid"] = {list = true, allow_holes = true},
["cat"] = list_param,
["nocat"] = boolean_param,
["nocap"] = boolean_param,
["sort"] = true,
["pagename"] = true, -- for testing or documentation purposes
["a"] = true,
["addl"] = true,
["def"] = true,
-- params that are only used when transcluding using {{tcl}}/{{transclude}}, to transmit information to {{tcl}}.
["tcl"] = true,
["tcl_t"] = list_param,
["tcl_tid"] = list_param,
["tcl_nolb"] = true,
["tcl_nolc"] = boolean_param,
["tcl_noextratext"] = boolean_param,
}
-- add "extra info" parameters
for _, extra_arg_spec in ipairs(export.extra_info_args) do
params[extra_arg_spec.arg] = list_param
end
if template_args.def == "" then
error("Cannot currently pass def= as an empty parameter; use def=- if you want to suppress the definition display")
end
local args = require("Module:parameters").process(template_args, params)
if args.a then
track("a")
end
data.args = args
local overall_place_spec = parse_overall_place_spec(data)
data.overall_place_spec = overall_place_spec
return get_def(data) .. (
args.nocat and "" or format_cats(args[1], export.get_cats(args, overall_place_spec), args.sort))
end
--[==[
Actual entry point of {{tl|place}}.
]==]
function export.show(frame)
return export.format {
template_args = frame:getParent().args,
}
end
return export