မော်ဂျူး:place
Documentation for this module may be created at မော်ဂျူး:place/doc
local export = {}
local m_links = require("Module:links")
local m_langs = require("Module:languages")
local m_strutils = require("Module:string utilities")
local m_debug = require("Module:debug")
local data = require("Module:place/data")
local rmatch = mw.ustring.match
local rfind = mw.ustring.find
local rsplit = mw.text.split
local cat_data = data.cat_data
local namespace = mw.title.getCurrentTitle().nsText
----------- Wikicode utility functions
-- Return a wikilink link {{l|language|text}}
local function link(text, language, id)
if not language or language == "" then
return text
end
return m_links.full_link({term = text, lang = m_langs.getByCode(language), id = id}, nil, true)
end
-- Return the category link for a category, given the language code and the
-- name of the category.
local function catlink(lang, text, sort_key)
return require("Module:utilities").format_categories({lang:getCode() .. ":" .. data.remove_links_and_html(text)}, lang, sort_key)
end
---------- Basic utility functions
-- Add the page to a tracking "category". To see the pages in the "category",
-- go to [[Template:tracking/place/PAGE]] and click on "What links here".
local function track(page)
m_debug.track("place/" .. page)
return true
end
local function ucfirst_all(text)
if text:find(" ") then
local parts = rsplit(text, " ", true)
for i, part in ipairs(parts) do
parts[i] = m_strutils.ucfirst(part)
end
return table.concat(parts, " ")
else
return m_strutils.ucfirst(text)
end
end
local function lc(text)
return mw.getContentLanguage():lc(text)
end
-- Fetches the synergy table from cat_data, which describes the format of
-- glosses consisting of <placetype1> and <placetype2>.
-- The parameters are tables in the format {placetype, placename, langcode}.
local function get_synergy_table(place1, place2)
if not place2 then
return nil
end
local pt_data = data.get_equiv_placetype_prop(place2[1], function(pt) return cat_data[pt] end)
if not pt_data or not pt_data.synergy then
return nil
end
if not place1 then
place1 = {}
end
local synergy = data.get_equiv_placetype_prop(place1[1], function(pt) return pt_data.synergy[pt] end)
return synergy or pt_data.synergy["default"]
end
-- Return the article that is used with a place type. It is fetched from the cat_data
-- table; if that doesn’t exist, "an" is given for words beginning with a vowel
-- and "a" otherwise.
-- If sentence == true, the first letter of the article is made upper-case.
local function get_placetype_article(placetype, sentence)
local art
local pt_data = data.get_equiv_placetype_prop(placetype, function(pt) return cat_data[pt] end)
if pt_data and pt_data.article then
art = pt_data.article
elseif placetype:find("^[aeiou]") then
art = "an"
else
art = "a"
end
if sentence then
art = m_strutils.ucfirst(art)
end
return art
end
---------- Argument parsing functions and utilities
-- Given a place spec (see parse_place_specs()) and a holonym spec (the return value
-- of split_holonym()), add a key/value into the place spec corresponding to the
-- placetype and placename of the holonym spec. For example, corresponding to the
-- holonym "country/Italy", a key "country" with the list value {"Italy"} will be
-- added to the place spec. If there is already a key with that place type, the new
-- placename will be added to the end of the value's list.
local function key_holonym_spec_into_place_spec(place_spec, holonym_spec)
if not holonym_spec[1] then
return place_spec
end
local equiv_placetypes = data.get_placetype_equivs(holonym_spec[1])
local placename = holonym_spec[2]
for _, equiv in ipairs(equiv_placetypes) do
local placetype = equiv.placetype
if not place_spec[placetype] then
place_spec[placetype] = {placename}
else
place_spec[placetype][table.getn(place_spec[placetype]) + 1] = placename
end
end
return place_spec
end
-- Implement "implications", i.e. where the presence of a given holonym causes additional
-- holonym(s) to be added. There are two types of implications, general implications
-- (which apply to both display and categorization) and category implications (which apply
-- only to categorization). PLACE_SPECS is the return value of parse_place_specs(), i.e.
-- one or more place specs, collectively describing the data passed to {{place}}.
-- IMPLICATION_DATA is the data used to implement the implications, i.e. a table indexed
-- by holonym placetype, each value of which is a table indexed by holonym place name,
-- each value of which is a list of "PLACETYPE/PLACENAME" holonyms to be added to the
-- end of the list of holonyms. SHOULD_CLONE specifies whether to clone a given place spec
-- before modifying it.
local function handle_implications(place_specs, implication_data, should_clone)
-- handle category implications
for n, spec in ipairs(place_specs) do
local lastarg = table.getn(spec)
local cloned = false
for c = 3, lastarg do
local imp_data = data.get_equiv_placetype_prop(spec[c][1], function(pt)
local implication = implication_data[pt] and implication_data[pt][data.remove_links_and_html(spec[c][2])]
if implication then
return implication
end
end)
if imp_data then
if should_clone and not cloned then
spec = mw.clone(spec)
cloned = true
place_specs[n] = spec
end
for i, holonym_to_add in ipairs(imp_data) do
local split_holonym = rsplit(holonym_to_add, "/", true)
if #split_holonym ~= 2 then
error("Invalid holonym in implications: " .. holonym_to_add)
end
local holonym_placetype, holonym_placename = split_holonym[1], split_holonym[2]
local new_holonym = {holonym_placetype, holonym_placename}
spec[table.getn(spec) + i] = new_holonym
key_holonym_spec_into_place_spec(spec, new_holonym)
end
end
end
end
end
-- Look up a placename in an alias table, handling links appropriately.
-- If the alias isn't found, return nil.
local function lookup_placename_alias(placename, aliases)
-- If the placename is a link, apply the alias inside the link.
-- This pattern matches both piped and unpiped links. If the link is not
-- piped, the second capture (linktext) will be empty.
local link, linktext = rmatch(placename, "^%[%[([^|%]]+)%|?(.-)%]%]$")
if link then
if linktext ~= "" then
local alias = aliases[linktext]
return alias and "[[" .. link .. "|" .. alias .. "]]" or nil
else
local alias = aliases[link]
return alias and "[[" .. alias .. "]]" or nil
end
else
return aliases[placename]
end
end
-- Split a holonym placename on commas but don't split on comma+space. This way, we split on
-- "Poland,Belarus,Ukraine" but keep "Tucson, Arizona" together.
local function split_holonym_placename(placename)
if placename:find(", ") then
local placenames = rsplit(placename, ",", true)
local retval = {}
for i, placename in ipairs(placenames) do
if i > 1 and placename:find("^ ") then
retval[#retval] = retval[#retval] .. "," .. placename
else
table.insert(retval, placename)
end
end
return retval
else
return rsplit(placename, ",", true)
end
end
-- Split a holonym (e.g. "continent/Europe" or "country/en:Italy" or "in southern"
-- or "r:suf/O'Higgins") into its components. Return value is
-- {PLACETYPE, PLACENAME, LANGCODE, MODIFIERS}, e.g. {"country", "Italy", "en", {}} or
-- {"region", "O'Higgins", nil, {"suf"}}. If there isn't a slash (e.g. "in southern"),
-- the first element 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(datum)
-- Don't use rsplit() in case of slash in holonym placename, e.g. Admaston/Bromley.
local holonym_placetype, holonym_placename = rmatch(datum, "^(.-)/(.*)$")
if holonym_placetype then
datum = {holonym_placetype, holonym_placename}
else
datum = {nil, datum}
end
-- Check for langcode before the holonym placename, but don't get tripped up by
-- Wikipedia links, which begin "[[w:...]]" or "[[wikipedia:]]".
local langcode, holonym_placename = rmatch(datum[2], "^([^%[%]]-):(.*)$")
if langcode then
datum[2] = holonym_placename
datum[3] = langcode
end
-- Check for modifiers after the holonym placetype.
if datum[1] then
local split_holonym_placetype = rsplit(datum[1], ":", true)
datum[1] = split_holonym_placetype[1]
local modifiers = {}
local i = 2
while true do
if split_holonym_placetype[i] then
table.insert(modifiers, split_holonym_placetype[i])
else
break
end
i = i + 1
end
datum[4] = modifiers
else
datum[4] = {}
end
if datum[1] then
datum[1] = data.placetype_aliases[datum[1]] or datum[1]
datum[2] = data.get_equiv_placetype_prop(datum[1],
function(pt) return data.placename_display_aliases[pt] and lookup_placename_alias(datum[2], data.placename_display_aliases[pt]) end
) or datum[2]
if not datum[3] then
datum[3] = "en"
end
end
if datum[1] and datum[2]:find(",") then
local placenames = split_holonym_placename(datum[2])
local retval = {}
for _, placename in ipairs(placenames) do
local holonym = {datum[1], placename, datum[3], datum[4]}
table.insert(retval, holonym)
end
return retval, true
else
return datum, false
end
end
-- Parse a "new-style" place spec, with placetypes and holonyms surrounded by <<...>>
-- amid otherwise raw text. Return value is a place spec, as documented in
-- parse_place_specs().
local function parse_new_style_place_spec(text)
local placetypes = {}
local segments = m_strutils.capturing_split(text, "<<(.-)>>")
local retval = {"foobar", true, order = {}}
for i, segment in ipairs(segments) do
if i % 2 == 1 then
table.insert(retval.order, {"raw", segment})
elseif segment:find("/") then
local holonym, is_multi = split_holonym(segment)
if is_multi then
for j, single_holonym in ipairs(holonym) do
if j > 1 then
if j == #holonym then
table.insert(retval.order, {"raw", " and "})
else
table.insert(retval.order, {"raw", ", "})
end
-- Signal that "the" needs to be added if appropriate
table.insert(single_holonym[4], "_art_")
end
table.insert(retval, single_holonym)
table.insert(retval.order, {"holonym", #retval})
key_holonym_spec_into_place_spec(retval, single_holonym)
end
else
table.insert(retval, holonym)
table.insert(retval.order, {"holonym", #retval})
key_holonym_spec_into_place_spec(retval, holonym)
end
else
-- see if the placetype segment is just qualifiers
local only_qualifiers = true
local split_segments = rsplit(segment, " ", true)
for _, split_segment in ipairs(split_segments) do
if not data.placetype_qualifiers[split_segment] then
only_qualifiers = false
break
end
end
table.insert(placetypes, {segment, only_qualifiers})
if only_qualifiers then
table.insert(retval.order, {"qualifier", segment})
else
table.insert(retval.order, {"placetype", segment})
end
end
end
local final_placetypes = {}
for i, placetype in ipairs(placetypes) do
if i > 1 and placetypes[i - 1][2] then
final_placetypes[#final_placetypes] = final_placetypes[#final_placetypes] .. " " .. placetypes[i][1]
else
table.insert(final_placetypes, placetypes[i][1])
end
end
retval[2] = final_placetypes
return retval
end
-- Process numeric args (except for the language code in 1=). The return value is one or
-- more "place specs", each one corresponding to a single semicolon-separated combination of
-- placetype + holonyms in the numeric arguments. A given place spec is a table
-- {"foobar", PLACETYPES, HOLONYM_SPEC, HOLONYM_SPEC, ..., HOLONYM_PLACETYPE={HOLONYM_PLACENAME, ...}, ...}.
-- For example, the call {{place|en|city|s/Pennsylvania|c/US}} will result in a place spec
-- {"foobar", {"city"}, {"state", "Pennsylvania"}, {"country", "United States"}, state={"Pennsylvania"}, country={"United States"}}.
-- Here, the placetype aliases "s" and "c" have been expanded into "state" and "country"
-- respectively, and the placename alias "US" has been expanded into "United States".
-- PLACETYPES is a list because there may be more than one (e.g. the call
-- {{place|en|city/and/county|s/California}} will result in a place spec
-- {"foobar", {"city", "and", "county"}, {"state", "California"}, state={"California"}})
-- and the value in the key/value pairs is likewise a list (e.g. the call
-- {{place|en|city|s/Kansas|and|s/Missouri}} will result in a place spec
-- {"foobar", {"city"}, {"state", "Kansas"}, {nil, "and"}, {"state", "Missouri"}, state={"Kansas", "Missouri"}}).
local function parse_place_specs(numargs)
local specs = {}
local c = 1
local cY = 1
local cX = 2
local last_was_new_style = false
while numargs[c] do
if numargs[c] == ";" or numargs[c]:find("^;[^ ]") then
if numargs[c] == ";" then
specs[cY].joiner = "; "
elseif numargs[c] == ";;" then
specs[cY].joiner = " "
else
local joiner = numargs[c]:sub(2)
if rfind(joiner, "^%a") then
specs[cY].joiner = " " .. joiner .. " "
else
specs[cY].joiner = joiner .. " "
end
end
cY = cY + 1
cX = 2
last_was_new_style = false
else
if numargs[c]:find("<<") then
if cX > 2 then
cY = cY + 1
cX = 2
end
specs[cY] = parse_new_style_place_spec(numargs[c])
last_was_new_style = true
cX = cX + 1
else
if last_was_new_style then
error("Old-style arguments cannot directly follow new-style place spec")
end
last_was_new_style = false
if cX == 2 then
local entry_placetypes = rsplit(numargs[c], "/", true)
for n, ept in ipairs(entry_placetypes) do
entry_placetypes[n] = data.placetype_aliases[ept] or ept
end
specs[cY] = {"foobar", entry_placetypes}
cX = cX + 1
else
local holonym, is_multi = split_holonym(numargs[c])
if is_multi then
for j, single_holonym in ipairs(holonym) do
if j > 1 then
-- Signal that "the" needs to be added if appropriate
table.insert(single_holonym[4], "_art_")
if j == #holonym then
specs[cY][cX] = {nil, "and", nil, {}}
cX = cX + 1
end
end
specs[cY][cX] = single_holonym
key_holonym_spec_into_place_spec(specs[cY], specs[cY][cX])
cX = cX + 1
end
else
specs[cY][cX] = holonym
key_holonym_spec_into_place_spec(specs[cY], specs[cY][cX])
cX = cX + 1
end
end
end
end
c = c + 1
end
handle_implications(specs, data.implications, false)
for _, spec in ipairs(specs) do
for _, entry_placetype in ipairs(spec[2]) do
track("entry-placetype/" .. entry_placetype)
local splits = data.split_qualifiers_from_placetype(entry_placetype, "no canon qualifiers")
for _, split in ipairs(splits) do
local prev_qualifier, this_qualifier, bare_placetype = unpack(split)
track("entry-placetype/" .. bare_placetype)
track("entry-qualifier/" .. this_qualifier)
end
end
cY = 3
while spec[cY] do
if spec[cY][1] then
track("holonym-placetype/" .. spec[cY][1])
end
cY = cY + 1
end
end
return specs
end
-------- Definition-generating functions
-- Return a string with the wikilinks to the English translations of the word.
local function get_translations(transl, ids)
local ret = {}
for i, t in ipairs(transl) do
table.insert(ret, link(t, "en", ids[i]))
end
return table.concat(ret, ", ")
end
-- Prepend the appropriate article if needed to LINKED_PLACENAME, where PLACENAME
-- is the corresponding unlinked placename and PLACETYPE its placetype.
local function get_holonym_article(placetype, placename, linked_placename)
placename = data.remove_links_and_html(placename)
local unlinked_placename = data.remove_links_and_html(linked_placename)
if unlinked_placename:find("^the ") then
return nil
end
local art = data.get_equiv_placetype_prop(placetype, function(pt) return data.placename_article[pt] and data.placename_article[pt][placename] end)
if art then
return art
end
art = data.get_equiv_placetype_prop(placetype, function(pt) return cat_data[pt] and cat_data[pt].holonym_article end)
if art then
return art
end
local universal_res = data.placename_the_re["*"]
for _, re in ipairs(universal_res) do
if unlinked_placename:find(re) then
return "the"
end
end
local matched = data.get_equiv_placetype_prop(placetype, function(pt)
local res = data.placename_the_re[pt]
if not res then
return nil
end
for _, re in ipairs(res) do
if unlinked_placename:find(re) then
return true
end
end
return nil
end)
if matched then
return "the"
end
return nil
end
-- Return the description of a holonym, with an extra article if necessary and in the
-- wikilinked display form if necessary.
-- Examples:
-- ({"country", "United States", "en", {}}, true, true) returns the template-expanded
-- equivalent of "the {{l|en|United States}}".
-- ({"region", "O'Higgins", "en", {"suf"}}, false, true) returns the template-expanded
-- equivalent of "{{l|en|O'Higgins}} region".
local function get_holonym_description(place, needs_article, display_form)
local ps = place[2]
local affix_type_pt_data, affix_type, affix, no_affix_strings, pt_equiv_for_affix_type, already_seen_affix
if not needs_article then
for _, mod in ipairs(place[4]) do
if mod == "_art_" then
needs_article = true
break
end
end
end
if display_form then
-- Implement display handlers.
local display_handler = data.get_equiv_placetype_prop(place[1], function(pt) return cat_data[pt] and cat_data[pt].display_handler end)
if display_handler then
ps = display_handler(place[1], place[2])
end
-- Implement adding an affix (prefix or suffix) based on the place type. The affix will be
-- added either if the place type's cat_data spec says so (by setting 'affix_type'), or if the
-- user explicitly called for this (e.g. by using 'r:suf/O'Higgins'). Before adding the affix,
-- however, we check to see if the affix is already present (e.g. the place type is "district"
-- and the place name is "Mission District"). If the place type explicitly calls for adding
-- an affix, it can override the affix to add (by setting 'affix') and/or override the strings
-- used for checking if the affix is already presen (by setting 'no_affix_strings').
affix_type_pt_data, pt_equiv_for_affix_type = data.get_equiv_placetype_prop(place[1],
function(pt) return cat_data[pt] and cat_data[pt].affix_type and cat_data[pt] end
)
if affix_type_pt_data then
affix_type = affix_type_pt_data.affix_type
affix = affix_type_pt_data.affix or pt_equiv_for_affix_type.placetype
no_affix_strings = affix_type_pt_data.no_affix_strings or lc(affix)
end
for _, mod in ipairs(place[4]) do
if (mod == "pref" or mod == "Pref" or mod == "suf" or mod == "Suf") and place[1] then
affix_type = mod
affix = place[1]
no_affix_strings = lc(affix)
break
end
end
already_seen_affix = no_affix_strings and data.check_already_seen_string(ps, no_affix_strings)
ps = link(ps, place[3])
if (affix_type == "suf" or affix_type == "Suf") and not already_seen_affix then
ps = ps .. " " .. (affix_type == "Suf" and ucfirst_all(affix) or affix)
end
end
if needs_article then
local article = get_holonym_article(place[1], place[2], ps)
if article then
ps = article .. " " .. ps
end
end
if display_form then
if (affix_type == "pref" or affix_type == "Pref") and not already_seen_affix then
ps = (affix_type == "Pref" and ucfirst_all(affix) or affix) .. " of " .. ps
if needs_article then
ps = "the " .. ps
end
end
end
return ps
end
-- Return a special description generated from a synergy table fetched from
-- the data module and two place tables.
local function get_synergic_description(synergy, place1, place2)
local desc = ""
if place1 then
if synergy.before then
desc = desc .. " " .. synergy.before
end
desc = desc .. " " .. get_holonym_description(place1, true, true)
end
if synergy.between then
desc = desc .. " " .. synergy.between
end
desc = desc .. " " .. get_holonym_description(place2, true, true)
if synergy.after then
desc = desc .. " " .. synergy.after
end
return desc
end
-- Return the preposition that should be used between the placetypes placetype1 and
-- placetype2 (i.e. "city >in< France.", "country >of< South America"
-- If there is no placetype2, a single whitespace is returned. Otherwise, the
-- preposition is fetched from the data module. If there isn’t any, the default
-- is "in".
-- The preposition is return with a whitespace before and after.
local function get_in_or_of(placetype1, placetype2)
if not placetype2 then
return " "
end
local preposition = "in"
local pt_data = data.get_equiv_placetype_prop(placetype1, function(pt) return cat_data[pt] end)
if pt_data and pt_data.preposition then
preposition = pt_data.preposition
end
return " " .. preposition .. " "
end
-- Return a string that contains the information of how a given place (place2)
-- should be formatted in the gloss, considering the entry’s place type, the
-- place preceding it in the template’s parameter (place1) and following it
-- (place3), and whether it is the first place (parameter 4 of the function).
local function get_contextual_holonym_description(entry_placetype, place1, place2, place3, first)
local desc = ""
local synergy = get_synergy_table(place2, place3)
if synergy then
return ""
end
synergy = get_synergy_table(place1, place2)
if first then
if place2[1] then
desc = desc .. get_in_or_of(entry_placetype, "")
elseif not place2[2]:find("^,") then
desc = desc .. " "
end
else
if not synergy then
if place1[1] and place2[2] ~= "and" and place2[2] ~= "in" then
desc = desc .. ","
end
if place2[1] or not place2[2]:find("^,") then
desc = desc .. " "
end
end
end
if not synergy then
desc = desc .. get_holonym_description(place2, first, true)
else
desc = desc .. get_synergic_description(synergy, place1, place2)
end
return desc
end
local function get_linked_placetype(placetype)
local linked_version = data.placetype_links[placetype]
if linked_version then
if linked_version == true then
return "[[" .. placetype .. "]]"
elseif linked_version == "w" then
return "[[w:" .. placetype .. "|" .. placetype .. "]]"
else
return linked_version
end
end
local sg_placetype = data.maybe_singularize(placetype)
if sg_placetype then
local linked_version = data.placetype_links[sg_placetype]
if linked_version then
if linked_version == true then
return "[[" .. sg_placetype .. "|" .. placetype .. "]]"
elseif linked_version == "w" then
return "[[w:" .. sg_placetype .. "|" .. placetype .. "]]"
else
return m_strutils.pluralize(linked_version)
end
end
end
return nil
end
-- Return the linked description of a placetype. This splits off any
-- qualifiers and displays them separately.
local function get_placetype_description(placetype)
local linked_version = get_linked_placetype(placetype)
if linked_version then
return linked_version
else
local splits = data.split_qualifiers_from_placetype(placetype)
local prefix = ""
for _, split in ipairs(splits) do
local prev_qualifier, this_qualifier, bare_placetype = unpack(split)
prefix = (prev_qualifier and prev_qualifier .. " " .. this_qualifier or this_qualifier) .. " "
local linked_version = get_linked_placetype(bare_placetype)
if linked_version then
return prefix .. " " .. linked_version
end
placetype = bare_placetype
end
return prefix .. placetype
end
end
-- Return the linked description of a qualifier (which may be multiple words).
local function get_qualifier_description(qualifier)
local splits = data.split_qualifiers_from_placetype(qualifier .. " foo")
local prev_qualifier, this_qualifier, bare_placetype = unpack(splits[#splits])
return prev_qualifier and prev_qualifier .. " " .. this_qualifier or this_qualifier
end
-- Return a string with extra information that is sometimes added to a
-- definition. This consists of the tag, a whitespace and the value (wikilinked
-- if it language contains a language code; if sentence == true, ". " is added
-- before the string and the first character is made upper case.
local function get_extra_info(tag, values, sentence, auto_plural, with_colon)
if not values then
return ""
end
if type(values) ~= "table" then
values = {values}
end
if #values == 0 then
return ""
end
if auto_plural and #values > 1 then
tag = m_strutils.pluralize(tag)
end
if with_colon then
tag = tag .. ":"
end
local linked_values = {}
for _, value in ipairs(values) do
-- Check for langcode before the holonym placename, but don't get tripped up by
-- Wikipedia links, which begin "[[w:...]]" or "[[wikipedia:]]".
local langcode, holonym_placename = rmatch(value, "^([^%[%]]-):(.*)$")
if langcode then
value = link(holonym_placename, langcode)
else
value = link(value, "en")
end
table.insert(linked_values, value)
end
local s = ""
if sentence then
s = s .. ". " .. m_strutils.ucfirst(tag)
else
s = s .. "; " .. tag
end
return s .. " " .. require("Module:table").serialCommaJoin(linked_values)
end
-- Get the full description of an old-style place spec (with separate arguments for
-- the placetype and each holonym).
local function get_old_style_gloss(args, spec, with_article, sentence)
local gloss = ""
-- The placetype used to determine whether "in" or "of" follows is the last placetype if there are
-- multiple slash-separated placetypes, but ignoring "and", "or" and parenthesized notes
-- such as "(one of 254)".
local placetype_for_in_or_of = nil
for n2, placetype in ipairs(spec[2]) do
if placetype == "and" then
gloss = gloss .. " and "
elseif placetype == "or" then
gloss = gloss .. " or "
elseif placetype:find("^%(") then
-- Check for placetypes beginning with a paren (so that things
-- like "{{place|en|county/(one of 254)|s/Texas}}" work).
gloss = gloss .. " " .. placetype
else
placetype_for_in_or_of = placetype
-- Join multiple placetypes with comma unless placetypes are already
-- joined with "and". We allow "the" to precede the second placetype
-- if they're not joined with "and" (so we get "city and county seat of ..."
-- but "city, the county seat of ...").
if n2 > 1 and spec[2][n2-1] ~= "and" and spec[2][n2-1] ~= "or" then
local article = get_placetype_article(placetype)
if article ~= "the" then
-- Temporary tracking. Formerly we didn't insert an article in this case.
track("multiple-placetypes-no-the")
end
gloss = gloss .. ", " .. article .. " "
end
gloss = gloss .. get_placetype_description(placetype)
end
end
if args["also"] then
gloss = gloss .. " and " .. args["also"]
end
local c = 3
while spec[c] do
local prev = nil
if c > 3 then
prev = spec[c-1]
else
prev = {}
end
gloss = gloss .. get_contextual_holonym_description(placetype_for_in_or_of, prev, spec[c], spec[c+1], (c == 3))
c = c + 1
end
if with_article then
gloss = (args["a"] or get_placetype_article(spec[2][1], sentence)) .. " " .. gloss
end
return gloss
end
-- Get the full description of a new-style place spec. New-style place specs are
-- specified with a single string containing raw text interspersed with placetypes
-- and holonyms surrounded by <<...>>.
local function get_new_style_gloss(args, spec, with_article)
local parts = {}
if with_article and args["a"] then
table.insert(parts, args["a"] .. " ")
end
for _, order in ipairs(spec.order) do
local segment_type, segment = order[1], order[2]
if segment_type == "raw" then
table.insert(parts, segment)
elseif segment_type == "placetype" then
table.insert(parts, get_placetype_description(segment))
elseif segment_type == "qualifier" then
table.insert(parts, get_qualifier_description(segment))
elseif segment_type == "holonym" then
table.insert(parts, get_holonym_description(spec[segment], false, true))
else
error("Internal error: Unrecognized segment type '" .. segment_type .. "'")
end
end
return table.concat(parts)
end
-- Return a string with the gloss (the description of the place itself, as
-- opposed to translations). If sentence == true, the gloss’s first letter is
-- made upper case and a period is added to the end.
local function get_gloss(args, specs, sentence)
if args["def"] then
return args["def"]
end
local glosses = {}
for n, spec in ipairs(specs) do
if spec.order then
table.insert(glosses, get_new_style_gloss(args, spec, n == 1))
else
table.insert(glosses, get_old_style_gloss(args, spec, n == 1, sentence))
end
if spec.joiner then
table.insert(glosses, spec.joiner)
end
end
local ret = {table.concat(glosses)}
table.insert(ret, get_extra_info("modern", args["modern"], false, false, false))
table.insert(ret, get_extra_info("official name", args["official"], sentence, "auto plural", "with colon"))
table.insert(ret, get_extra_info("capital", args["capital"], sentence, "auto plural", "with colon"))
table.insert(ret, get_extra_info("largest city", args["largest city"], sentence, "auto plural", "with colon"))
table.insert(ret, get_extra_info("capital and largest city", args["caplc"], sentence, false, "with colon"))
local placetype = specs[1][2][1]
if placetype == "county" or placetype == "counties" then
placetype = "county seat"
elseif placetype == "parish" or placetype == "parishes" then
placetype = "parish seat"
elseif placetype == "borough" or placetype == "boroughs" then
placetype = "borough seat"
else
placetype = "seat"
end
table.insert(ret, get_extra_info(placetype, args["seat"], sentence, "auto plural", "with colon"))
table.insert(ret, get_extra_info("shire town", args["shire town"], sentence, "auto plural", "with colon"))
return table.concat(ret)
end
-- Return the definition line.
local function get_def(args, specs)
if #args["t"] > 0 then
return get_translations(args["t"], args["tid"]) .. " (" .. get_gloss(args, specs, false) .. ")"
else
return get_gloss(args, specs, true)
end
end
---------- Functions for the category wikicode
--[=[
The code in this section finds the categories to which a given place belongs. The algorithm
works off of a place spec (which specifies the entry placetype(s) and holonym(s); see
parse_place_specs()). Iterating over each entry placetype, it proceeds as follows:
(1) Look up the placetype in the `cat_data`, which comes from [[Module:place/data]]. Note that
the entry in `cat_data` that specifies the category or categories to add may directly
correspond to the entry placetype as specified in the place spec. For example, if the
entry placetype is "small town", the placetype whose data is fetched will be "town" since
"small" is a recognized qualifier and there is no entry in `cat_data` for "small town".
As another example, if the entry placetype is "administrative capital", the placetype
whose data will be fetched will be "capital city" because there's no entry in `cat_data`
for "administrative capital" but there is an entry in `placetype_equivs` in
[[Module:place/data]] that maps "administrative capital" to "capital city" for
categorization purposes.
(2) The value in `cat_data` is a two-level table. The outer table is indexed by the holonym
itself (e.g. "country/Brazil") or by "default", and the inner indexed by the holonym's
placetype (e.g. "country") or by "itself". Note that most frequently, if the outer table
is indexed by a holonym, the inner table will be indexed only by "itself", while if the
outer table is indexed by "default", the inner table will be indexed by one or more holonym
placetypes, meaning to generate a category for all holonyms of this placetype. But this
is not necessarily the case.
(3) Iterate through the holonyms, from left to right, finding the first holonym that matches
(in both placetype and placename) a key in the outer table. If no holonym matches any key,
then if a key "default" exists, use that; otherwise, if a key named "fallback" exists,
specifying a placetype, use that placetype to fetch a new `cat_data` entry, and start over
with step (1); otherwise, don't categorize.
(4) Iterate again through the holonyms, from left to right, finding the first holonym whose
placetype matches a key in the inner table. If no holonym matches any key, then if a key
"itself" exists, use that; otherwise, check for a key named "fallback" at the top level of
the `cat_data` entry and, if found, proceed as in step (3); otherwise don't categorize.
(5) The resulting value found is a list of category specs. Each category spec specifies a
category to be added. In order to understand how category specs are processed, you have to
understand the concept of the 'triggering holonym'. This is the holonym that matched an
inner key in step (4), if any; else, the holonym that matched an outer key in step (3),
if any; else, there is no triggering holonym. (The only time this happens when there are
category specs is when the outer key is "default" and the inner key is "itself".)
(6) Iterate through the category specs and construct a category from each one. Each category
spec is one of the following:
(a) A string, such as "Seas", Districts of England" or "Cities in +++". If "+++" is
contained in the string, it will be substituted with the placename of the triggering
holonym. If there is no triggering holonym, an error is thrown. This is then prefixed
with the language code specified in the first argument to the call to {{place}}.
For example, if the triggering holonym is "country/Brazil", the category spec is
"Cities in +++" and the template invocation was {{place|en|...}}, the resulting
category will be [[:Category:en:Cities in Brazil]].
(b) The value 'true'. If there is a triggering holonym, the spec "PLACETYPES in +++" or
"PLACETYPES of +++" is constructed. (Here, PLACETYPES is the plural of the entry
placetype whose cat_data is being used, which is not necessarily the same as the entry
placetype specified by the user; see the discussion above. The choice of "in" or "of"
is based on the value of the "preposition" key at the top level of the entry in
`cat_data`, defaulting to "in".) This spec is then processed as above. If there is no
triggering holonym, the simple spec "PLACETYPES" is constructed (where PLACETYPES is as
above).
For example, consider the following entry in cat_data:
["municipality"] = {
preposition = "of",
...
["country/Brazil"] = {
["state"] = {"Municipalities of +++, Brazil", "Municipalities of Brazil"},
["country"] = {true},
},
...
}
If the user uses a template call {{place|pt|municipality|s/Amazonas|c/Brazil}}, the
categories [[:Category:pt:Municipalities of Amazonas, Brazil]] and
[[:Category:pt:Municipalities of Brazil]] will be generated. This is because the outer key
"country/Brazil" matches the second holonym "c/Brazil" (by this point, the alias "c" has
been expanded to "country"), and the inner key "state" matches the first holonym "s/Amazonas",
which serves as the triggering holonym and is used to replace the +++ in the first category
spec.
Now imagine the user uses the template call {{place|en|small municipality|c/Brazil}}. There
is no entry in `cat_data` for "small municipality", but "small" is a recognized qualifier,
and there is an entry in `cat_data` for "municipality", so that entry's data is used. Now,
the second holonym "c/Brazil" will match the outer key "country/Brazil" as before, but in
this case the second holonym will also match the inner key "country" and will serve as the
triggering holonym. The cat spec 'true' will be expanded to "Municipalities of +++", using
the placetype "municipality" corresponding to the entry in `cat_data` (not the user-specified
placetype "small municipality"), and the preposition "of", as specified in the `cat_data`
entry. The +++ will then be expanded to "Brazil" based on the triggering holonym, the language
code "en" will be prepended, and the final category will be
[[:Category:en:Municipalities of Brazil]].
]=]
-- Find the appropriate category specs for a given place spec; e.g. for the call
-- {{place|en|city|s/Pennsylvania|c/US}} which results in the place spec
-- {"foobar", {"city"}, {"state", "Pennsylvania"}, {"country", "United States"}, state={"Pennsylvania"}, country={"United States"}},
-- the return value might be be "city", {"Cities in +++, USA"}, {"state", "Pennsylvania"}, "outer"
-- (i.e. four values are returned; see below). See the comment at the top of the section for a
-- description of category specs and the overall algorithm.
--
-- More specifically, given the following arguments:
-- (1) the entry placetype (or equivalent) used to look up the category data in cat_data;
-- (2) the value of cat_data[placetype] for this placetype;
-- (3) the full place spec as documented in parse_place_specs() (used only for its holonyms);
-- (4) an optional overriding holonym to use, in place of iterating through the holonyms;
-- (5) if an overriding holonym was specified, either "inner" or "outer" to indicate which loop to override;
-- find the holonyms that match the outer-level and inner-level keys in the `cat_data` entry
-- according to the algorithm described in the top-of-section comment, and return the resulting
-- category specs. Four values are actually returned:
--
-- CATEGORY_SPECS, ENTRY_PLACETYPE, TRIGGERING_HOLONYM, INNER_OR_OUTER
--
-- where
--
-- (1) CATEGORY_SPECS is a list of category specs as described above;
-- (2) ENTRY_PLACETYPE is the placetype that should be used to construct categories when 'true'
-- is one of the returned category specs (normally the same as the `entry_placetype` passed
-- in, but will be different when a "fallback" key exists and is used);
-- (3) TRIGGERING_HOLONYM is the triggering holonym (see the comment at the top of the section), in the
-- standard {PLACETYPE, PLACENAME} format, or nil if there was no triggering holonym;
-- (4) INNER_OR_OUTER is "inner" if the triggering holonym matched in the inner loop (whether or not a
-- holonym matched the outer loop), or "outer" if the triggering holonym matched in the outer loop
-- only, or nil if no triggering holonym.
local function find_cat_specs(entry_placetype, entry_placetype_data, place_spec, overriding_holonym, override_inner_outer)
local inner_data = nil
local outer_triggering_holonym
local function fetch_inner_data(holonym_to_match)
local holonym_placetype, holonym_placename = holonym_to_match[1], holonym_to_match[2]
holonym_placename = data.resolve_cat_aliases(holonym_placetype, holonym_placename)
local inner_data = data.get_equiv_placetype_prop(holonym_placetype,
function(pt) return entry_placetype_data[(pt or "") .. "/" .. holonym_placename] end)
if inner_data then
return inner_data
end
if entry_placetype_data.cat_handler then
local inner_data = data.get_equiv_placetype_prop(holonym_placetype,
function(pt) return entry_placetype_data.cat_handler(pt, holonym_placename, place_spec) end)
if inner_data then
return inner_data
end
end
return nil
end
if overriding_holonym and override_inner_outer == "outer" then
inner_data = fetch_inner_data(overriding_holonym)
outer_triggering_holonym = overriding_holonym
else
local c = 3
while place_spec[c] do
inner_data = fetch_inner_data(place_spec[c])
if inner_data then
outer_triggering_holonym = place_spec[c]
break
end
c = c + 1
end
end
if not inner_data then
inner_data = entry_placetype_data["default"]
end
-- If we didn't find a matching place spec, and there's a fallback, look it up.
-- This is used, for example, with "rural municipality", which has special cases for
-- some provinces of Canada and otherwise behaves like "municipality".
if not inner_data and entry_placetype_data.fallback then
return find_cat_specs(entry_placetype_data.fallback, cat_data[entry_placetype_data.fallback], place_spec, overriding_holonym, override_inner_outer)
end
if not inner_data then
return nil, entry_placetype, nil, nil
end
local function fetch_cat_specs(holonym_to_match)
return data.get_equiv_placetype_prop(holonym_to_match[1], function(pt) return inner_data[pt] end)
end
if overriding_holonym and override_inner_outer == "inner" then
local cat_specs = fetch_cat_specs(overriding_holonym)
if cat_specs then
return cat_specs, entry_placetype, overriding_holonym, "inner"
end
else
local c2 = 3
while place_spec[c2] do
local cat_specs = fetch_cat_specs(place_spec[c2])
if cat_specs then
return cat_specs, entry_placetype, place_spec[c2], "inner"
end
c2 = c2 + 1
end
end
local cat_specs = inner_data["itself"]
if cat_specs then
return cat_specs, entry_placetype, outer_triggering_holonym, "outer"
end
-- If we didn't find a matching key in the inner data, and there's a fallback, look it up, as above.
-- This is used, for example, with "rural municipality", which has special cases for
-- some provinces of Canada and otherwise behaves like "municipality".
if entry_placetype_data.fallback then
return find_cat_specs(entry_placetype_data.fallback, cat_data[entry_placetype_data.fallback], place_spec, overriding_holonym, override_inner_outer)
end
return nil, entry_placetype, nil, nil
end
-- Return the plural of a word and makes its first letter upper case.
-- The plural is fetched from the data module; if it doesn’t find one,
-- the 'pluralize' function from [[Module:string utilities]] is called,
-- which pluralizes correctly in almost all cases.
local function get_cat_plural(word)
local pt_data, equiv_placetype_and_qualifier = data.get_equiv_placetype_prop(word, function(pt) return cat_data[pt] end)
if pt_data then
word = pt_data.plural or m_strutils.pluralize(equiv_placetype_and_qualifier.placetype)
else
word = m_strutils.pluralize(word)
end
return m_strutils.ucfirst(word)
end
-- Turn a list of category specs (see comment at section top) into the corresponding wikicode.
-- It is given the following arguments:
-- (1) the language object (param 1=)
-- (2) the category specs retrieved using find_cat_specs()
-- (3) the entry placetype used to fetch the entry in `cat_data`
-- (4) the triggering holonym used to fetch the category specs (see top-of-section comment), in
-- the format of indices 3, 4, ... of the place spec data, as described in
-- parse_place_specs()); or nil if no triggering holonym
-- The return value is constructed as described in the top-of-section comment.
local function cat_specs_to_category_wikicode(lang, cat_specs, entry_placetype, holonym, sort_key)
local all_cats = ""
if holonym then
local holonym_placetype, holonym_placename = holonym[1], holonym[2]
holonym_placename = data.resolve_cat_aliases(holonym_placetype, holonym_placename)
holonym = {holonym_placetype, holonym_placename}
for _, cat_spec in ipairs(cat_specs) do
local cat
if cat_spec == true then
cat = get_cat_plural(entry_placetype) .. get_in_or_of(entry_placetype, holonym_placetype) .. " +++"
else
cat = cat_spec
end
cat = cat:gsub("%+%+%+", get_holonym_description(holonym, true, false))
all_cats = all_cats .. catlink(lang, cat, sort_key)
end
else
for _, cat_spec in ipairs(cat_specs) do
local cat
if cat_spec == true then
cat = get_cat_plural(entry_placetype)
else
cat = cat_spec
if cat:find("%+%+%+") then
error("Category '" .. cat .. "' contains +++ but there is no holonym to substitute")
end
end
all_cats = all_cats .. catlink(lang, cat, sort_key)
end
end
return all_cats
end
-- Return a string containing the category wikicode that should be added to the entry, given the
-- place spec (which specifies the entry placetype(s) and holonym(s); see parse_place_specs()) and
-- a particular entry placetype (e.g. "city"). Note that only the holonyms from the place spec are
-- looked at, not the entry placetypes in the place spec.
local function get_cat(lang, place_spec, entry_placetype, sort_key)
local entry_pt_data, equiv_entry_placetype_and_qualifier = data.get_equiv_placetype_prop(entry_placetype, function(pt) return cat_data[pt] end)
-- Check for unrecognized placetype.
if not entry_pt_data then
return ""
end
local equiv_entry_placetype = equiv_entry_placetype_and_qualifier.placetype
-- Find the category specs (see top-of-file comment) corresponding to the holonym(s) in the place spec.
local cat_specs, returned_entry_placetype, triggering_holonym, inner_outer =
find_cat_specs(equiv_entry_placetype, entry_pt_data, place_spec)
-- Check if no category spec could be found. This happens if the innermost table in the category data
-- doesn't match any holonym's placetype and doesn't have an "itself" entry.
if not cat_specs then
return ""
end
-- Generate categories for the category specs found.
local cat = cat_specs_to_category_wikicode(lang, cat_specs, returned_entry_placetype, triggering_holonym, sort_key)
-- If there's a triggering holonym (see top-of-file comment), also generate categories for other holonyms
-- of the same placetype, so that e.g. {{place|en|city|s/Kansas|and|s/Missouri|c/USA}} generates both
-- [[:Category:en:Cities in Kansas, USA]] and [[:Category:en:Cities in Missouri, USA]].
if triggering_holonym then
local c2 = 2
local other_holonyms_of_same_placetype = place_spec[triggering_holonym[1]]
while other_holonyms_of_same_placetype[c2] do
local overriding_holonym = {triggering_holonym[1], other_holonyms_of_same_placetype[c2]}
local other_cat_specs, other_returned_entry_placetype, other_triggering_holonym, other_inner_outer =
find_cat_specs(equiv_entry_placetype, entry_pt_data, place_spec, overriding_holonym, inner_outer)
if other_cat_specs then
cat = cat .. cat_specs_to_category_wikicode(lang, other_cat_specs, other_returned_entry_placetype,
other_triggering_holonym, sort_key)
end
c2 = c2 + 1
end
end
return cat
end
-- Iterate through each type of place given in parameter 2 (a list of place specs, as documented
-- in parse_place_specs()) and return a string with the links to all categories that need to be
-- added to the entry.
local function get_cats(lang, place_specs, additional_cats, sort_key)
local cats = {}
handle_implications(place_specs, data.cat_implications, true)
for n1, place_spec in ipairs(place_specs) do
for n2, placetype in ipairs(place_spec[2]) do
if placetype ~= "and" then
table.insert(cats, get_cat(lang, place_spec, placetype, sort_key))
end
end
-- Also add base categories for the holonyms listed (e.g. a category like
-- 'en:Places in Merseyside, England'). This is handled through the special placetype "*".
table.insert(cats, get_cat(lang, place_spec, "*", sort_key))
end
for _, addl_cat in ipairs(additional_cats) do
table.insert(cats, catlink(lang, addl_cat, sort_key))
end
return table.concat(cats)
end
----------- Main entry point
function export.show(frame)
local params = {
[1] = {required = true},
[2] = {required = true, list = true},
["t"] = {list = true},
["tid"] = {list = true, allow_holes = true},
["cat"] = {list = true},
["sort"] = {},
["a"] = {},
["also"] = {},
["def"] = {},
["modern"] = {list = true},
["official"] = {list = true},
["capital"] = {list = true},
["largest city"] = {list = true},
["caplc"] = {},
["seat"] = {list = true},
["shire town"] = {list = true},
}
local args = require("Module:parameters").process(frame:getParent().args, params)
local lang = require("Module:languages").getByCode(args[1]) or error("The language code \"" .. args[1] .. "\" is not valid.")
local place_specs = parse_place_specs(args[2])
return get_def(args, place_specs) .. get_cats(lang, place_specs, args["cat"], args["sort"])
end
return export