Wiksiyonaryo
tlwiktionary
https://tl.wiktionary.org/wiki/Wiksiyonaryo:Unang_Pahina
MediaWiki 1.48.0-wmf.1
case-sensitive
Midya
Natatangi
Usapan
Tagagamit
Usapang tagagamit
Wiksiyonaryo
Usapang Wiksiyonaryo
Talaksan
Usapang talaksan
MediaWiki
Usapang MediaWiki
Padron
Usapang padron
Tulong
Usapang tulong
Kategorya
Usapang kategorya
TimedText
TimedText talk
Module
Module talk
Event
Event talk
Module:audio
828
33027
178546
174853
2026-10-29T00:45:01Z
GinawaSaHapon
3989
178546
Scribunto
text/plain
local export = {}
local headword_data_module = "Module:headword/data"
local IPA_module = "Module:IPA"
local labels_module = "Module:labels"
local links_module = "Module:links"
local parameters_module = "Module:parameters"
local qualifier_module = "Module:qualifier"
local references_module = "Module:references"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local template_styles_module = "Module:TemplateStyles"
local utilities_module = "Module:utilities"
local audio_styles_css = "audio/styles.css"
local function track(page)
require("Module:debug/track")("audio/" .. page)
return true
end
local function wrap_qualifier_css(text, suffix)
return require(qualifier_module).wrap_qualifier_css(text, suffix)
end
--[==[
Display a box that can be used to play an audio file. `data` is a table containing the following fields:
* `lang` ('''required'''): language object for the audio files;
* `file` ('''required'''): file containing the audio;
* `caption`: Caption to display before the audio box; normally {"Audio"}, and does not usually need to be changed;
* `nocaption`: If specified, don't display the caption;
* `q`: {nil} or a list of left regular qualifier strings, formatted using {format_qualifier()} in [[Module:qualifier]]
and displayed before the audio box and after the caption (and any accent qualifiers);
* `qq`: {nil} or a list of right regular qualifier strings, displayed directly after the audio box (and after any
accent qualifiers);
* `a`: {nil} or a list of left accent qualifier strings, formatted using {format_qualifiers()} in
[[Module:accent qualifier]] and displayed before the audio box and after the caption;
* `aa`: {nil} or a list of right accent qualifier strings, displayed directly after the homophone in question;
* `refs`: {nil} or a list of references or reference specs to add directly after the audio box; the value of a list item
is either a string containing the reference text (typically a call to a citation template such as {{tl|cite-book}}, or
a template wrapping such a call), or an object with fields `text` (the reference text), `name` (the name of the
reference, as in {{cd|<nowiki><ref name="foo">...</ref></nowiki>}} or {{cd|<nowiki><ref name="foo" /></nowiki>}})
and/or `group` (the group of the reference, as in {{cd|<nowiki><ref name="foo" group="bar">...</ref></nowiki>}} or
{{cd|<nowiki><ref name="foo" group="bar"/></nowiki>}}); this uses a parser function to format the reference
appropriately and insert a footnote number that hyperlinks to the actual reference, located in the
{{cd|<nowiki><references /></nowiki>}} section;
* `text`: Text of the audio snippet; if specified, should be an object of the form passed to {full_link()} in
[[Module:links]], including a `lang` field containing the language of the text (usually the same as `data.lang`);
displayed before the audio box, after any regular and accent qualifiers;
* `IPA`: IPA of the audio snippet, or a list of IPA specs; if specified, should be surrounded by slashes or brackets,
and will be processed using {format_IPA_multiple()} in [[Module:IPA]] and displayed before the audio box, after any
regular and accent qualifiers and after the text of the audio snippet, if given;
* `nocat`: If true, suppress categorization;
* `sort`: Sort key for categorization.
]==]
function export.format_audio(data)
local langname = data.lang:getFullName()
local cats = { langname .. " na salitang may pagbigkas na tunog" } --TLCHANGE " terms with audio pronunciation"
local function format_a(a)
if a and a[1] then
return require(labels_module).show_labels {
lang = data.lang,
labels = a,
mode = "accent",
nocat = true,
open = false,
close = false,
no_track_already_seen = true,
}
end
return nil
end
local function format_q(q)
if q and q[1] then
return require(qualifier_module).format_qualifier(q, false, false)
end
return nil
end
local function make_td_if(text)
if text == "" then
return text
end
return "<td>" .. text .. "</td>"
end
-- Generate the full text preceding the audio box.
local pretext_parts = {}
local function ins(text)
table.insert(pretext_parts, text)
end
local formatted_accent_labels, formatted_qualifiers, formatted_text, formatted_ipa
formatted_accent_labels = format_a(data.a)
formatted_qualifiers = format_q(data.q)
if data.text then
formatted_text = require(links_module).full_link(data.text, "term", true)
end
if data.IPA then
local ipa_cats
local ipa = data.IPA
if type(ipa) == "string" then
ipa = {ipa}
end
local ipa_items = {}
for _, ipa_item in ipairs(ipa) do
table.insert(ipa_items, {pron = ipa_item})
end
formatted_ipa, ipa_cats = require(IPA_module).format_IPA_multiple(data.lang, ipa_items, nil, "no count", "raw")
if ipa_cats[1] then
require(table_module).extend(cats, ipa_cats)
end
end
local has_qual = formatted_accent_labels or formatted_qualifiers
if not data.nocaption then
-- Track uses of caption (3=). Over time as we eliminate most of them, we can use this to find and
-- eliminate the remainder.
if data.caption then
track("caption")
end
ins(data.caption or "Tunog") --TLCHANGE "Audio"
if has_qual then
ins(" " .. wrap_qualifier_css("(", "brac"))
end
end
if formatted_accent_labels then
ins(formatted_accent_labels)
if formatted_qualifiers then
ins(wrap_qualifier_css(",", "comma") .. " ")
end
end
if formatted_qualifiers then
ins(formatted_qualifiers)
end
if has_qual then
if not data.nocaption then
ins(wrap_qualifier_css(")", "brac"))
end
end
if (formatted_text or formatted_ipa) and (has_qual or not data.nocaption) then
ins(wrap_qualifier_css(";", "semicolon") .. " ")
end
if formatted_text then
ins(formatted_text)
if formatted_ipa then
ins(" ")
end
end
ins(formatted_ipa)
if not data.nocaption then
ins(wrap_qualifier_css(":", "colon"))
end
local pretext = make_td_if(table.concat(pretext_parts))
-- Generate the full text following the audio box.
local posttext_parts = {}
local function ins(text)
table.insert(posttext_parts, text)
end
local formatted_post_accent_labels = format_a(data.aa)
local formatted_post_qualifiers = format_q(data.qq)
local formatted_references = data.refs and require(references_module).format_references(data.refs) or nil
if formatted_references then
ins(formatted_references)
end
if formatted_post_accent_labels or formatted_post_qualifiers then
if formatted_references then
ins(" ")
end
ins(wrap_qualifier_css("(", "brac"))
if formatted_post_accent_labels then
ins(formatted_post_accent_labels)
if formatted_post_qualifiers then
ins(wrap_qualifier_css(",", "comma") .. " ")
end
end
if formatted_post_qualifiers then
ins(formatted_post_qualifiers)
end
ins(wrap_qualifier_css(")", "brac"))
end
if data.bad then
table.insert(cats, langname .. " terms with nonstandard or incorrect audio pronunciations")
ins(" " .. require(qualifier_module).wrap_css("Note: this pronunciation may be nonstandard or incorrect: " .. data.bad, "bad-audio-note"))
end
local posttext = make_td_if(table.concat(posttext_parts))
local template = [=[
<tr>%s<td class="audiofile">[[File:%s|noicon|175px]]</td><td class="audiometa" style="font-size: 80%%;">([[:File:%s|file]])</td>%s</tr>]=]
local text = template:format(pretext, data.file, data.file, posttext)
text = '<table class="audiotable" style="vertical-align: middle; display: inline-block; list-style: none; line-height: 1em; border-collapse: collapse; margin: 0;">' .. text .. "</table>"
local stylesheet = require(template_styles_module)(audio_styles_css)
local categories =
data.nocat and "" or
cats[1] and require(utilities_module).format_categories(cats, data.lang, data.sort) or ""
return stylesheet .. text .. categories
end
--[==[
FIXME: Old entry point for formatting multiple audios in a single table. Not used anywhere and needs rewriting to the
standard of format_audio().
Meant to be called from a module. `data` is a table containing the following fields:
<pre>
{
lang = LANGUAGE_OBJECT,
audios = {{file = "FILENAME", qualifiers = nil or {"QUALIFIER", "QUALIFIER", ...}}, ...},
caption = nil or "CAPTION"
}
</pre>
Here:
* `lang` is a language object.
* `audios` is the list of audio files to display. FILENAME is the name of the audio file without a namespace.
QUALIFIER is a qualifier string to display after the specific audio file in question, formatted using
{format_qualifier()} in [[Module:qualifier]].
* `caption`, if specified, adds a caption before the audio file.
]==]
function export.format_multiple_audios(data)
local audiocats = { data.lang:getFullName() .. " na salitang may pagbigkas na tunog" } --TLCHANGE " terms with audio pronunciation"
local rows = { }
local caption = data.caption
for _, audio in ipairs(data.audios) do
local qualifiers = audio.qualifiers
local function repl(key)
if key == "file" then
return audio.file
elseif key == "caption" then
if not caption then return "" end
return "<td rowspan=" .. #data.audios .. ">" .. caption .. ":</td>"
elseif key == "qualifiers" then
if not qualifiers or not qualifiers[1] then return "" end
return "<td>" .. require(qualifier_module).format_qualifier(qualifiers) .. "</td>"
end
end
local template = [=[
<tr>{{{caption}}}
<td class="audiofile">[[File:{{{file}}}|noicon|175px]]</td>
<td class="audiometa" style="font-size: 80%;">([[:File:{{{file}}}|file]])</td>
{{{qualifiers}}}</tr>]=]
local text = (mw.ustring.gsub(template, "{{{([a-z0-9_:]+)}}}", repl))
table.insert(rows, text)
caption = nil
end
local function repl(key)
if key == "rows" then
return table.concat(rows, "\n")
end
end
local template = [=[
<table class="audiotable" style="vertical-align: middle; display: inline-block; list-style: none; line-height: 1em; border-collapse: collapse;">
{{{rows}}}
</table>
]=]
local stylesheet = require(template_styles_module)(audio_styles_css)
local text = mw.ustring.gsub(template, "{{{([a-z0-9_:]+)}}}", repl)
local categories =
data.nocat and "" or
#audiocats > 0 and require(utilities_module).format_categories(audiocats, data.lang, data.sort) or ""
-- remove newlines due to HTML generator bug in MediaWiki(?) - newlines in tables cause list items to not end correctly
text = mw.ustring.gsub(text, "\n", "")
return stylesheet .. text .. categories
end
--[==[
Construct the `text` object passed into {format_audio()}, from raw-ish arguments (essentially, the output of {process()}
in [[Module:parameters]]). On entry, `args` contains the following fields:
* `lang` ('''required'''): Language object.
* `text`: Text. If this isn't defined and neither are any of `gloss`, `tr`, `ts`, `pos`, `lit` or `genders`, the
function returns {nil}.
* `gloss`: Gloss of text.
* `tr`: Manual transliteration of text.
* `ts`: Transcription of text.
* `pos`: Part of speech of text.
* `lit`: Literal meaning of text.
* `genders`: List of gender/number spec(s) of text.
* `sc`: Optional script object of text (rarely needs to be set).
* `pagename`: Pagename; used in place of `text` when `text` is unset but other text-related parameters are set.
If not specified, taken from the actual pagename.
]==]
function export.construct_audio_textobj(args)
local textobj
if args.text or args.gloss or args.tr or args.ts or args.pos or args.lit or args.genders and args.genders[1] then
local text = args.text or args.pagename or mw.loadData("Module:headword/data").pagename
textobj = {
lang = args.lang,
alt = wrap_qualifier_css("“", "quote") .. text .. wrap_qualifier_css("”", "quote"),
gloss = args.gloss,
tr = args.tr,
ts = args.ts,
pos = args.pos,
lit = args.lit,
genders = args.genders,
sc = args.sc,
}
end
return textobj
end
--[==[
Entry point for {{tl|audio}} template.
]==]
function export.show(frame)
local parent_args = frame:getParent().args
local compat = parent_args.lang
local offset = compat and 0 or 1
local params = {
[compat and "lang" or 1] = {required = true, type = "language", default = "en"},
[1 + offset] = {required = true, default = "Example.ogg"},
[2 + offset] = {},
["q"] = {type = "qualifier"},
["qq"] = {type = "qualifier"},
["a"] = {type = "labels"},
["aa"] = {type = "labels"},
["ref"] = {type = "references"},
["IPA"] = {sublist = true},
["text"] = {},
["t"] = {},
["gloss"] = {alias_of = "t"},
["tr"] = {},
["ts"] = {},
["pos"] = {},
["lit"] = {},
["g"] = {sublist = true},
["sc"] = {type = "script"},
["bad"] = {},
["nocat"] = {type = "boolean"},
["sort"] = {},
["pagename"] = {},
}
local args = require(parameters_module).process(parent_args, params)
local lang = args[compat and "lang" or 1]
-- Needed in construct_audio_textobj().
args.lang = lang
local textobj = export.construct_audio_textobj(args)
local caption = args[2 + offset]
local nocaption
if caption == "-" then
caption = nil
nocaption = true
end
if caption then
-- Remove final colon if given, to avoid two colons.
caption = caption:gsub(":$", "")
end
local data = {
lang = lang,
file = args[1 + offset],
caption = caption,
nocaption = nocaption,
q = args.q,
qq = args.qq,
a = args.a,
aa = args.aa,
refs = args.ref,
text = textobj,
IPA = args.IPA,
bad = args.bad,
nocat = args.nocat,
sort = args.sort,
}
return export.format_audio(data)
end
return export
4nyz9yxho01wji4px6l35f2uzo5j69s
Module:ast-IPA
828
36641
178538
171487
2026-10-29T00:33:22Z
GinawaSaHapon
3989
178538
Scribunto
text/plain
--Adapted from [[Module:es-pronunc]] by [[User:Benwing2]]
local export = {}
local m_IPA = require("Module:IPA")
local m_table = require("Module:table")
local audio_module = "Module:audio"
local put_module = "Module:parse utilities"
local parameters_module = "Module:parameters"
local force_cat = false -- for testing
local lang = require("Module:languages").getByCode("ast")
local u = mw.ustring.char
local rfind = mw.ustring.find
local rsubn = mw.ustring.gsub
local rmatch = mw.ustring.match
local rsplit = mw.text.split
local ulower = mw.ustring.lower
local uupper = mw.ustring.upper
local usub = mw.ustring.sub
local ulen = mw.ustring.len
local unfd = mw.ustring.toNFD
local unfc = mw.ustring.toNFC
local AC = u(0x0301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local CFLEX = u(0x0302) -- circumflex = ̂
local TILDE = u(0x0303) -- tilde = ̃
local DIA = u(0x0308) -- diaeresis = ̈
local DOT = u(0x0323) --underdot
local SYLDIV = u(0xFFF0) -- used to represent a user-specific syllable divider (.) so we won't change it
local vowel = "aeiouüöAEIOUÜÖø" -- vowel; include y so we get single-word y correct and for syllabifying from spelling
local V = "[" .. vowel .. "]" -- vowel class
local accent = AC .. GR .. CFLEX
local accent_c = "[" .. accent .. "]"
local stress = AC .. GR
local stress_c = "[" .. AC .. GR .. "]"
local ipa_stress = "ˈˌ"
local ipa_stress_c = "[" .. ipa_stress .. "]"
local sylsep = "%-." .. SYLDIV -- hyphen included for syllabifying from spelling
local sylsep_c = "[" .. sylsep .. "]"
local wordsep = "# "
local separator_not_wordsep = accent .. ipa_stress .. sylsep
local separator = separator_not_wordsep .. wordsep
local separator_c = "[" .. separator .. "]"
local C = "[^" .. vowel .. separator .. "]" -- consonant class including h
local C_NOT_H = "[^" .. vowel .. separator .. "h]" -- consonant class not including h
local C_OR_WORDSEP = "[^" .. vowel .. separator_not_wordsep .. "]" -- consonant class including h, or word separator
local T = "[^" .. vowel .. "lrɾjw" .. separator .. "]" -- obstruent or nasal
local unstressed_words = require("Module:table").listToSet({ --feel free to add more unstressed words
"el", "la", "les", "lo", "los", -- definite articles
"que", "quien", "onde", "au", "le", "nos", "os", -- relative pronouns
"mos", "nos", "so", "te", -- unstressed pronouns
"y", "ya", "o", -- conjunctions
"con", "a", "de", "en", "pa", "per", "por", "sin", "tres" --prepositions
})
-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
local retval = rsubn(term, foo, bar)
return retval
end
-- version of rsubn() that returns a 2nd argument boolean indicating whether
-- a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = rsubn(term, foo, bar)
return retval, nsubs > 0
end
-- apply rsub() repeatedly until no change
local function rsub_repeatedly(term, foo, bar)
while true do
local new_term = rsub(term, foo, bar)
if new_term == term then
return term
end
term = new_term
end
end
local function decompose(text)
-- decompose everything but ḷ, , ñ and ü/ö
text = unfd(text)
text = rsub(text, ".[" .. TILDE .. DIA .. DOT .. "]", {
["n" .. TILDE] = "ñ",
["N" .. TILDE] = "Ñ",
["u" .. DIA] = "ü",
["U" .. DIA] = "Ü",
["o" .. DIA] = "ö",
["O" .. DIA] = "Ö",
["l" .. DOT] = "ḷ",
["L" .. DOT] = "Ḷ",
["h" .. DOT] = "ḥ",
["H" .. DOT] = "Ḥ"
})
return text
end
local function split_on_comma(term)
if term:find(",%s") then
return require(put_module).split_on_comma(term)
else
return rsplit(term, ",")
end
end
-- Remove any HTML from the formatted text and resolve links, since the extra characters don't contribute to the
-- displayed length.
local function convert_to_raw_text(text)
text = rsub(text, "<.->", "")
if text:find("%[%[") then
text = require("Module:links").remove_links(text)
end
return text
end
-- Return the approximate displayed length in characters.
local function textual_len(text)
return ulen(convert_to_raw_text(text))
end
-- Main syllable-division algorithm
local function syllabify_from_spelling_or_pronun(text, is_spelling)
-- Part 1: Divide before the last consonant in a cluster of consonants between vowels (but don't divide a VhV
-- sequence; [[prohibir]] should be prohi.bir). Then move the syllable division marker leftwards over clusters that
-- can form onsets.
text = rsub_repeatedly(text, "(" .. V .. accent_c .. "*)(" .. C_NOT_H .. V .. ")", "%1.%2")
text = rsub_repeatedly(text, "(" .. V .. accent_c .. "*" .. C .. "+)(" .. C .. V .. ")", "%1.%2")
-- NOTE: When run on pronun, we have already eliminated c and v, but not when run on spelling.
-- When run on pronun, don't include r, which at this point represents the trill.
local cluster_r = is_spelling and "rɾ" or "ɾ"
-- Don't divide Cl or Cr where C is a stop or fricative, except for dl.
text = rsub(text, "([pbfvkctg])%.([l" .. cluster_r .. "])", ".%1%2")
text = text:gsub("d%.([" .. cluster_r .. "])", ".d%1")
-- Don't divide ch, sh, ph, th, dh, fh, kh or gh. Do allow bh to be divided ([[subhumano]], [[subhúmedo]], etc.).
text = rsub(text, "([csptdfkg])%.h", ".%1h")
-- Don't divide rr.
text = rsub(text, "r.r", ".rr")
-- Part 2: Divide hiatuses.
text = rsub_repeatedly(text, "([aeoAEO]" .. accent_c .. "*)(h?[aeo])", "%1.%2")
text = rsub_repeatedly(text, "([aeoAEO]" .. accent_c .. "*)(h?" .. V .. stress_c .. ")", "%1.%2")
text = rsub(text, "([iuüIUÜ]" .. stress_c .. ")(h?[aeoiu])", "%1.%2")
text = rsub_repeatedly(text, "([iuüIUÜ]" .. stress_c .. ")(h?" .. V .. stress_c .. ")", "%1.%2")
text = rsub_repeatedly(text, "([iI]" .. accent_c .. "*)(h?i)", "%1.%2")
text = rsub_repeatedly(text, "([uU]" .. accent_c .. "*)(h?u)", "%1.%2")
return text
end
local function syllabify_from_spelling(text)
text = decompose(text)
-- start at FFF1 because FFF0 is used for SYLDIV
-- Temporary replacements for characters we want treated as default consonants. The C and related consonant regexes
-- treat all unknown characters as consonants.
local TEMP_QU = u(0xFFF4)
local TEMP_QU_CAPS = u(0xFFF5)
local TEMP_GU = u(0xFFF6)
local TEMP_GU_CAPS = u(0xFFF7)
local TEMP_H = u(0xFFF8)
local TEMP_CV = u(0x0400)
local TEMP_CV_CAPS = u(0x0401)
local TEMP_TS = u(0x0402)
local TEMP_TS_CAPS = u(0x0403)
local TEMP_YY = u(0x0404)
local TEMP_YY_CAPS = u(0x0405)
local TEMP_LL = u(0xFFFB)
local TEMP_LL_CAPS = u(0xFFFC)
-- Change user-specified . into SYLDIV so we don't shuffle it around when dividing into syllables.
text = rsub(text, "ḷḷ", TEMP_CV)
text = rsub(text, "Ḷḷ", TEMP_CV_CAPS)
text = rsub(text, "ts", TEMP_TS)
text = rsub(text, "Ts", TEMP_TS_CAPS)
text = rsub(text, "yy", TEMP_YY)
text = rsub(text, "Yy", TEMP_YY_CAPS)
text = rsub(text, "ll", TEMP_LL)
text = rsub(text, "Ll", TEMP_LL_CAPS)
text = text:gsub("%.", SYLDIV)
-- We don't want to break -sh- except in desh-, e.g. [[deshuesar]], [[deshonra]], [[deshecho]]. Normally, -sh- is
-- automatically preserved, so we replace the h with a temporary symbol to avoid this.
text = text:gsub("^([Dd]es)h", "%1" .. TEMP_H)
text = text:gsub("([ %-][Dd]es)h", "%1" .. TEMP_H)
-- qu mostly handled correctly automatically, but not in quietud
text = rsub(text, "qu(" .. V .. ")", TEMP_QU .. "%1")
text = rsub(text, "Qu(" .. V .. ")", TEMP_QU_CAPS .. "%1")
text = rsub(text, "gu(" .. V .. ")", TEMP_GU .. "%1")
text = rsub(text, "Gu(" .. V .. ")", TEMP_GU_CAPS .. "%1")
local vowel_to_glide = { ["i"] = TEMP_I, ["u"] = TEMP_U }
text = rsub_repeatedly(text, "(.*" .. V .. accent_c .. "*)(h?)([iu])(" .. V .. ")",
function (v1, h, iu, v2) return v1 .. "." .. h .. vowel_to_glide[iu] .. v2 end
)
text = syllabify_from_spelling_or_pronun(text, "is spelling")
text = text:gsub(SYLDIV, ".")
text = text:gsub(TEMP_QU, "qu")
text = text:gsub(TEMP_QU_CAPS, "Qu")
text = text:gsub(TEMP_GU, "gu")
text = text:gsub(TEMP_GU_CAPS, "Gu")
text = text:gsub(TEMP_H, "h")
text = text:gsub(TEMP_CV, "ḷḷ")
text = text:gsub(TEMP_CV_CAPS, "Ḷḷ")
text = text:gsub(TEMP_TS, "ts")
text = text:gsub(TEMP_TS_CAPS, "Ts")
text = text:gsub(TEMP_YY, "yy")
text = text:gsub(TEMP_YY_CAPS, "Yy")
text = text:gsub(TEMP_LL, "ll")
text = text:gsub(TEMP_LL_CAPS, "Ll")
text = unfc(text)
return text
end
-- Generate the IPA of a given respelling
function export.IPA(text, phonetic)
text = ulower(text or mw.title.getCurrentTitle().text)
-- decompose everything but ñ and ü
text = decompose(text)
-- convert commas and en/en dashes to IPA foot boundaries
text = rsub(text, "%s*[,–—]%s*", " | ")
-- question mark or exclamation point in the middle of a sentence -> IPA foot boundary
text = rsub(text, "([^%s])%s*[¡!¿?]%s*([^%s])", "%1 | %2")
-- canonicalize multiple spaces and remove leading and trailing spaces
local function canon_spaces(text)
text = rsub(text, "%s+", " ")
text = rsub(text, "^ ", "")
text = rsub(text, " $", "")
return text
end
text = canon_spaces(text)
-- Make prefixes unstressed unless they have an explicit stress marker; also make certain monosyllabic words unstressed
local words = rsplit(text, " ")
for i, word in ipairs(words) do
if rfind(word, "%-$") and not rfind(word, accent_c) or unstressed_words[word] then
-- add CFLEX to the last vowel not the first one
words[i] = rsub(word, "^(.*" .. V .. ")", "%1" .. CFLEX)
end
end
text = table.concat(words, " ")
-- Convert hyphens to spaces
text = rsub(text, "%-", " ")
text = rsub(text, "@", "-")
-- canonicalize multiple spaces again, which may have been introduced by hyphens
text = canon_spaces(text)
-- now eliminate punctuation
text = rsub(text, "[¡!¿?']", "")
-- put # at word beginning and end and double ## at text/foot boundary beginning/end
text = rsub(text, " | ", "# | #")
text = "##" .. rsub(text, " ", "# #") .. "##"
--determining whether "y" is a consonant or a vowel
text = rsub(text, "yy", "ķ")
text = rsub(text, "y(" .. V .. ")", "ɟ%1") -- not the real sound
text = rsub(text, "y", "i")
--x
text = rsub(text, "x", "ʃ")
--c, g, q
text = rsub(text, "c([ie])", "θ%1")
text = rsub(text, "g([ie])", "x%1") -- handled after <x> = /ʃ/
text = rsub(text, "gu([ie])", "g%1")
text = rsub(text, "gü([ie])", "gu%1") --it's actually /w/, but we will use <u> to keep the syllabification function simpler
text = rsub(text, "ng([^aeiouüwhlr])", "n%1") -- [[Bangkok]], [[ángstrom]], [[branding]]
text = rsub(text, "qu([ie])", "k%1") --fixme: should other q-s and ü-s be handled here?
text = rsub(text, "z", "θ")
--digraphs
text = rsub(text, "ḷḷ", "đ")
text = rsub(text, "ts", "ç") --not the real sound
text = rsub(text, "ch", "č") --not the real symbol
text = rsubb(text, "ll", "ʎ")
text = rsub(text, "#p([st])", "#%1") -- [[psicología]]
text = rsub(text, "[cjñrvy]",
{ ["c"] = "k", ["j"] = "x", ["ñ"] = "ɲ", ["r"] = "ɾ", ["v"] = "b", ["y"] = "j"})
-- trill in #r, lr, nr, sr, rr, zr too?
text = rsub(text, "ɾɾ", "r")
text = rsub(text, "([#lnsz])ɾ", "%1r")
--other double consonants
text = rsub(text, "(" .. C .. ")%1", "%1")
--assimilation of nasals and sibilants
text = rsub(text, "n([# .]*[bpm])", "m%1")
if phonetic then
text = rsub(text, "n([# .]*[kg])", "ŋ%1")
end
--double uu and ii
text = rsub(text, "uu" .. AC, "u.u" .. AC)
text = rsub(text, "ii" .. AC, "i.i" .. AC)
text = rsubb(text, "uu", "wu")
text = rsubb(text, "ii", "ji")
-- convert i/u between vowels to glide
local vowel_to_glide = { ["i"] = "j", ["u"] = "w" }
text = rsub_repeatedly(text, "(.*" .. V .. accent_c .. "*h?)([iu])(" .. V .. ")",
function (v1, iu, v2) return v1 .. vowel_to_glide[iu] .. v2 end
)
--syllable division
text = syllabify_from_spelling_or_pronun(text, false)
--diphthongs
text = rsub(text, "i([aeoöu])", "j%1")
text = rsub(text, "u([aeioö])", "w%1")
local accent_to_stress_mark = { [AC] = "ˈ", [GR] = "ˌ", [CFLEX] = "" }
local function accent_word(word, syllables)
-- Now stress the word. If any accent exists in the word (including ^ indicating an unaccented word),
-- put the stress mark(s) at the beginning of the indicated syllable(s). Otherwise, apply the default
-- stress rule.
if rfind(word, accent_c) then
for i = 1, #syllables do
syllables[i] = rsub(syllables[i], "^(.*)(" .. accent_c .. ")(.*)$",
function(pre, accent, post) return accent_to_stress_mark[accent] .. pre .. post end
)
end
else
-- Default stress rule. Words without vowels (e.g. IPA foot boundaries) don't get stress.
if #syllables > 1 and (rfind(word, "[^" .. vowel .. "ns#]#") or rfind(word, C .. "[ns]#")) or #syllables == 1 and rfind(word, V) then
syllables[#syllables] = "ˈ" .. syllables[#syllables]
elseif #syllables > 1 and rfind(word, "[aeo][iu][ns#]") then
syllables[#syllables] = "ˈ" .. syllables[#syllables]
elseif #syllables > 1 then
syllables[#syllables - 1] = "ˈ" .. syllables[#syllables - 1]
end
end
end
local words = rsplit(text, " ")
for j, word in ipairs(words) do
-- accentuation
local syllables = rsplit(word, "%.")
if rfind(word, "men%.te#") then
local mente_syllables
-- Words ends in -mente (converted above to ménte); add a stress to the preceding portion
mente_syllables = {}
mente_syllables[2] = table.remove(syllables)
mente_syllables[1] = table.remove(syllables)
accent_word(table.concat(syllables, "."), syllables)
accent_word(table.concat(mente_syllables, "."), mente_syllables)
table.insert(syllables, mente_syllables[1])
table.insert(syllables, mente_syllables[2])
else
accent_word(word, syllables)
end
-- Vowels are nasalized if followed by nasal in same syllable.
if phonetic then
for i = 1, #syllables do
-- first check for two vowels (veinte)
syllables[i] = rsub(syllables[i], "(" .. V .. ")(" .. V .. ")([mnɲ])",
"%1" .. TILDE .. "%2" .. TILDE .. "%3")
-- then for one vowel
syllables[i] = rsub(syllables[i], "(" .. V .. ")([mnɲ])", "%1" .. TILDE .. "%2")
end
end
-- Reconstruct the word and make all primary stresses but the last one be secondary
words[j] = table.concat(syllables, ".")
words[j] = rsub_repeatedly(words[j], "ˈ(.+)ˈ", "ˌ%1ˈ")
end
text = table.concat(words, " ")
text = rsub(text, "%-", "")
-- suppress syllable mark before IPA stress indicator
text = rsub(text, "%.(" .. ipa_stress_c .. ")", "%1")
--phonetic transcription
if phonetic then
-- θ, s, f before voiced consonants
local voiced = "mnɲbdɟgʎl"
local r = "ɾr"
local tovoiced = {
["θ"] = "θ̬",
["s"] = "z",
["ʃ"] = "ʒ",
}
local function voice(sound, following)
return tovoiced[sound] .. following
end
text = rsub(text, "([θsʃ])(" .. separator_c .. "*[" .. voiced .. r .. "])", voice)
-- fricative vs. stop allophones; first convert stops to fricatives, then back to stops
-- after nasals and sometimes after l
local stop_to_fricative = { ["b"] = "β", ["d"] = "ð", ["ɟ"] = "ʝ" }
local fricative_to_stop = { ["β"] = "b", ["ð"] = "d", ["ʝ"] = "ɟ" }
text = rsub(text, "[bdɟg]", stop_to_fricative)
text = rsub(text, "([mnŋɲ]" .. separator_c .. "*)([βɣ])",
function(nasal, fricative)
return nasal .. fricative_to_stop[fricative]
end
)
text = rsub(text, "([lʎmnɲ]" .. separator_c .. "*)([ð])",
function(nasal_l, fricative)
return nasal_l .. fricative_to_stop[fricative]
end
)
text = rsub(text, "([mnɲ]" .. separator_c .. "*)([ʝ])",
function(nasal_l, fricative)
return nasal_l .. fricative_to_stop[fricative]
end
)
text = rsub(text, "(##" .. ipa_stress_c .. "*)([βɣðʝ])",
function(stress, fricative)
return stress .. fricative_to_stop[fricative]
end
)
text = rsub(text, "[td]", { ["t"] = "t̪", ["d"] = "d̪" })
--dental s/z
local dental = "dtθð"
local todental = {
["s"] = "s̪",
["z"] = "z̪",
}
local function dent(sound, following)
return todental[sound] .. following
end
text = rsub(text, "([sz])(" .. separator_c .. "*[" .. dental .. "])", dent)
-- nasal assimilation before consonants
local labiodental, dentialveolar, dental, palatal, velar, alveolar = "ɱ", "n̪", "n̟", "ɲ", "ŋ", "n"
local nasal_assimilation = {
["f"] = labiodental,
["t"] = dentialveolar, ["d"] = dentialveolar,
["θ"] = dental,
["ĉ"] = palatal,
["ɲ"] = palatal,
["ʃ"] = palatal,
["ʒ"] = palatal,
["ɟ"] = palatal, ["ʎ"] = palatal,
["k"] = velar, ["x"] = velar, ["ɣ"] = velar,
["s"] = alveolar, ["z"] = alveolar, ["l"] = alveolar, ["r"] = alveolar, ["ɾ"] = alveolar, ["n"] = alveolar,
}
text = rsub(text, "n(" .. separator_c .. "*)(.)",
function(stress, following)
return (nasal_assimilation[following] or "n") .. stress .. following
end
)
--velar ŋ in final position before vowels
text = rsub(text, "n#", "ŋ#")
text = rsub(text, "ŋ(" .. separator_c .. "*)(.)",
function(stress, following)
return (nasal_assimilation[following] or "ŋ") .. stress .. following
end
)
-- lateral assimilation before consonants
text = rsub(text, "l(" .. separator_c .. "*)(.)",
function(stress, following)
local l = "l"
if following == "t" or following == "d" then
-- dentialveolar
l = "l̪"
elseif following == "θ" then
-- dental
l = "l̟"
elseif following == "ĉ" or following == "ɲ" or following == "ɟ" or following == "ʎ" or following == "ʃ" then
-- palatal
l = "ʎ"
end
return l .. stress .. following
end)
--vowels
text = rsub(text, "eu", "øu")
-- voiced fricatives are actually approximants
text = rsub(text, "([βðɣ])", "%1̞")
end
--final conversions
text = rsub(text, "([aeouãẽõũö][iĩ])", "%1̯")
text = rsub(text, "([aeioãẽĩõö][uũ])", "%1̯")
local final_conversions = {
["h"] = "",
["č"] = "t͡ʃ",
["g"] = "ɡ",
["ķ"] = "kʲ",
["ç"] = "t͡s",
["đ"] = "t͡ʂ",
["ḥ"] = "h",
["ɟ"] = phonetic and "ɟ͡ʝ" or "ʝ",
["ö"] = "ø"
}
text = rsub(text, "[hčđgķçđḥɟö]", final_conversions)
text = rsub(text, "#", "")
text = unfc(text)
return {text = text}
end
local function express_all_styles(dodialect)
local ret = {
pronun = {},
expressed_styles = {},
}
dodialect(ret)
table.insert(ret.expressed_styles, {
styles = {{pronun = ret.pronun,}},
})
return ret
end
local function format_all_styles(expressed_styles, format_style)
for i, style_group in ipairs(expressed_styles) do
style_group.formatted, style_group.formatted_len =
format_style(style_group.styles[1].tag, style_group.styles[1], i == 1)
end
local lines = {}
for i, style_group in ipairs(expressed_styles) do
table.insert(lines, style_group.formatted)
end
return table.concat(lines, "\n")
end
local function dodialect_pronun(args, ret)
ret.pronun = {}
for i, term in ipairs(args.terms) do
local phonemic, phonetic
if term.raw then
phonemic = term.raw_phonemic
phonetic = term.raw_phonetic
else
phonemic = export.IPA(term.term, false)
phonetic = export.IPA(term.term, true)
phonemic = phonemic.text
phonetic = phonetic.text
end
local refs
if not term.ref then
refs = nil
else
refs = {}
for _, refspec in ipairs(term.ref) do
local this_refs = require("Module:references").parse_references(refspec)
for _, this_ref in ipairs(this_refs) do
table.insert(refs, this_ref)
end
end
end
ret.pronun[i] = {
raw = term.raw,
phonemic = phonemic,
phonetic = phonetic,
refs = refs,
q = term.q,
qq = term.qq,
a = term.a,
aa = term.aa,
}
end
end
local function generate_pronun(args)
local function this_dodialect_pronun(ret)
dodialect_pronun(args, ret)
end
local ret = express_all_styles(this_dodialect_pronun)
local function format_style(tag, expressed_style, is_first)
local pronunciations = {}
local formatted_pronuns = {}
local function ins(formatted_part)
table.insert(formatted_pronuns, formatted_part)
end
-- Loop through each pronunciation.
for j, pronun in ipairs(expressed_style.pronun) do
-- Add tag to left qualifiers if first one
-- FIXME: Consider using accent qualifier for the tag instead.
local qs = pronun.q
if j == 1 and tag then
if qs then
qs = m_table.deepCopy(qs)
table.insert(qs, tag)
else
qs = {tag}
end
end
local first_pronun = #pronunciations + 1
if not pronun.phonemic and not pronun.phonetic then
error("Internal error: Saw neither phonemic nor phonetic pronunciation")
end
if pronun.phonemic then
local slash_pron = "/" .. pronun.phonemic:gsub("%.", "") .. "/"
table.insert(pronunciations, {
pron = slash_pron,
})
ins(slash_pron)
end
if pronun.phonetic then -- missing if 'raw:/.../' given
local bracket_pron = "[" .. pronun.phonetic .. "]"
table.insert(pronunciations, {
pron = bracket_pron,
})
ins(bracket_pron)
end
local last_pronun = #pronunciations
if qs then
pronunciations[first_pronun].q = qs
end
if pronun.a then
pronunciations[first_pronun].a = pronun.a
end
if j > 1 then
pronunciations[first_pronun].separator = ", "
ins(", ")
end
if pronun.qq then
pronunciations[last_pronun].qq = pronun.qq
end
if pronun.aa then
pronunciations[last_pronun].aa = pronun.aa
end
if qs or pronun.qq or pronun.a or pronun.aa then
-- Note: This inserts the actual formatted qualifier text, including HTML and such, but the later call
-- to textual_len() removes all HTML and reduces links.
ins(require("Module:pron qualifier").format_qualifiers {
lang = lang,
text = "",
q = qs,
qq = pronun.qq,
a = pronun.a,
aa = pronun.aa,
})
end
if pronun.refs then
pronunciations[last_pronun].refs = pronun.refs
-- Approximate the reference using a footnote notation. This will be slightly inaccurate if there are
-- more than nine references but that is rare.
ins(string.rep("[1]", #pronun.refs))
end
if first_pronun ~= last_pronun then
pronunciations[last_pronun].separator = " "
ins(" ")
end
end
local bullet = string.rep("*", args.bullets) .. " "
-- Here we construct the formatted line in `formatted`, and also try to construct the equivalent without HTML
-- and wiki markup in `formatted_for_len`, so we can compute the approximate textual length for use in sizing
-- the toggle box with the "more" button on the right.
local pre = is_first and args.pre and args.pre .. " " or ""
local post = is_first and args.post and " " .. args.post or ""
local formatted = bullet .. pre .. m_IPA.format_IPA_full { lang = lang, items = pronunciations, separator = "" } .. post
local formatted_for_len = bullet .. pre .. "IPA(key): " .. table.concat(formatted_pronuns) .. post
return formatted, textual_len(formatted_for_len)
end
ret.text = format_all_styles(ret.expressed_styles, format_style)
return ret
end
local function parse_respelling(respelling, pagename, parse_err)
local raw_respelling = respelling:match("^raw:(.*)$")
if raw_respelling then
local raw_phonemic = raw_respelling:match("^/(.*)/ %[(.*)%]$")
if not raw_phonemic then
raw_phonemic = raw_respelling:match("^/(.*)/$")
end
if not raw_phonemic then
parse_err(("Unable to parse raw respelling '%s', should be one of /.../, [...] or /.../ [...]")
:format(raw_respelling))
end
return {
raw = true,
raw_phonemic = raw_phonemic,
}
end
if respelling == "+" then
respelling = pagename
end
return {term = respelling}
end
-- Return the number of syllables of a phonemic representation, which should have syllable dividers in it but no hyphens.
local function get_num_syl_from_phonemic(phonemic)
-- Maybe we should just count vowels instead of the below code.
phonemic = rsub(phonemic, "|", " ") -- remove IPA foot boundaries
local words = rsplit(phonemic, " +")
for i, word in ipairs(words) do
-- IPA stress marks are syllable divisions if between characters; otherwise just remove.
word = rsub(word, "(.)[ˌˈ](.)", "%1.%2")
word = rsub(word, "[ˌˈ]", "")
words[i] = word
end
-- There should be a syllable boundary between words.
phonemic = table.concat(words, ".")
return ulen(rsub(phonemic, "[^.]", "")) + 1
end
-- Get the rhyme by truncating everything up through the last stress mark + any following consonants, and remove
-- syllable boundary markers.
local function convert_phonemic_to_rhyme(phonemic)
-- NOTE: This works because the phonemic vowels are just [aeiou] possibly with diacritics that are separate
-- Unicode chars. If we want to handle things like ɛ or ɔ we need to add them to `vowel`.
return rsub(rsub(phonemic, ".*[ˌˈ]", ""), "^[^" .. vowel .. "]*", ""):gsub("%.", ""):gsub("t͡ʃ", "tʃ"):gsub("d͡ʒ", "dʒ")
end
local function split_syllabified_spelling(spelling)
return rsplit(spelling, "%.")
end
-- "Align" syllabification to original spelling by matching character-by-character, allowing for extra syllable and
-- accent markers in the syllabification. If we encounter an extra syllable marker (.), we allow and keep it. If we
-- encounter an extra accent marker in the syllabification, we drop it. In any other case, we return nil indicating
-- the alignment failed.
local function align_syllabification_to_spelling(syllab, spelling)
local result = {}
local syll_chars = rsplit(decompose(syllab), "")
local spelling_chars = rsplit(decompose(spelling), "")
local i = 1
local j = 1
while i <= #syll_chars or j <= #spelling_chars do
local ci = syll_chars[i]
local cj = spelling_chars[j]
if ci == cj then
table.insert(result, ci)
i = i + 1
j = j + 1
elseif ci == "." then
table.insert(result, ci)
i = i + 1
elseif ci == AC or ci == GR or ci == CFLEX then
-- skip character
i = i + 1
else
-- non-matching character
return nil
end
end
if i <= #syll_chars or j <= #spelling_chars then
-- left-over characters on one side or the other
return nil
end
return unfc(table.concat(result))
end
local function generate_hyph_obj(term)
return {syllabification = term, hyph = split_syllabified_spelling(term)}
end
-- Word should already be decomposed.
local function word_has_vowels(word)
return rfind(word, V)
end
local function all_words_have_vowels(term)
local words = rsplit(decompose(term), "[ %-]")
for i, word in ipairs(words) do
-- Allow empty word; this occurs with prefixes and suffixes.
if word ~= "" and not word_has_vowels(word) then
return false
end
end
return true
end
local function should_generate_rhyme_from_respelling(term)
local words = rsplit(decompose(term), " +")
return #words == 1 and -- no if multiple words
not words[1]:find(".%-.") and -- no if word is composed of hyphenated parts (e.g. [[Austria-Hungría]])
not words[1]:find("%-$") and -- no if word is a prefix
not (words[1]:find("^%-") and words[1]:find(CFLEX)) and -- no if word is an unstressed suffix
word_has_vowels(words[1]) -- no if word has no vowels (e.g. a single letter)
end
local function should_generate_rhyme_from_ipa(ipa)
return not ipa:find("%s") and word_has_vowels(decompose(ipa))
end
local function dodialect_specified_rhymes(rhymes, hyphs, parsed_respellings, rhyme_ret)
rhyme_ret.pronun = {}
for _, rhyme in ipairs(rhymes) do
local num_syl = rhyme.num_syl
local no_num_syl = false
-- If user explicitly gave the rhyme but didn't explicitly specify the number of syllables, try to take it from
-- the hyphenation.
if not num_syl then
num_syl = {}
for _, hyph in ipairs(hyphs) do
if should_generate_rhyme_from_respelling(hyph.syllabification) then
local this_num_syl = 1 + ulen(rsub(hyph.syllabification, "[^.]", ""))
m_table.insertIfNot(num_syl, this_num_syl)
else
no_num_syl = true
break
end
end
if no_num_syl or #num_syl == 0 then
num_syl = nil
end
end
-- If that fails and term is single-word, try to take it from the phonemic.
if not no_num_syl and not num_syl then
for _, parsed in ipairs(parsed_respellings) do
for _, pronun in pairs(parsed.pronun.pronun) do
-- Check that pronun.phonemic exists (it may not if raw phonetic-only pronun is given).
if pronun.phonemic then
if not should_generate_rhyme_from_ipa(pronun.phonemic) then
no_num_syl = true
break
end
-- Count number of syllables by looking at syllable boundaries (including stress marks).
local this_num_syl = get_num_syl_from_phonemic(pronun.phonemic)
m_table.insertIfNot(num_syl, this_num_syl)
end
end
if no_num_syl then
break
end
end
if no_num_syl or #num_syl == 0 then
num_syl = nil
end
end
table.insert(rhyme_ret.pronun, {
rhyme = rhyme.rhyme,
num_syl = num_syl,
qualifiers = rhyme.qualifiers,
})
end
end
local function parse_pron_modifier(arg, parse_err, generate_obj, param_mods, no_split_on_comma)
local retval = {}
if arg:find("<") then
local insert = { store = "insert" }
param_mods.q = insert
param_mods.qq = insert
param_mods.a = insert
param_mods.aa = insert
return require(put_module).parse_inline_modifiers(arg, {
param_mods = param_mods,
generate_obj = generate_obj,
parse_err = parse_err,
splitchar = not no_split_on_comma and "," or nil,
})
elseif no_split_on_comma then
table.insert(retval, generate_obj(arg))
else
for _, term in ipairs(split_on_comma(arg)) do
table.insert(retval, generate_obj(term))
end
end
return retval
end
local function parse_rhyme(arg, parse_err)
local function generate_obj(term)
return {rhyme = term}
end
local param_mods = {
s = {
item_dest = "num_syl",
convert = function(arg, parse_err)
local nsyls = rsplit(arg, ",")
for i, nsyl in ipairs(nsyls) do
if not nsyl:find("^[0-9]+$") then
parse_err("Number of syllables '" .. nsyl .. "' should be numeric")
end
nsyls[i] = tonumber(nsyl)
end
return nsyls
end,
},
}
return parse_pron_modifier(arg, parse_err, generate_obj, param_mods)
end
local function parse_hyph(arg, parse_err)
-- None other than qualifiers
local param_mods = {}
return parse_pron_modifier(arg, parse_err, generate_hyph_obj, param_mods)
end
local function parse_homophone(arg, parse_err)
local function generate_obj(term)
return {term = term}
end
local param_mods = {
t = {
-- We need to store the <t:...> inline modifier into the "gloss" key of the parsed term,
-- because that is what [[Module:links]] (called from [[Module:homophones]]) expects.
item_dest = "gloss",
},
gloss = {},
pos = {},
alt = {},
lit = {},
id = {},
g = {
-- We need to store the <g:...> inline modifier into the "genders" key of the parsed term,
-- because that is what [[Module:links]] (called from [[Module:homophones]]) expects.
item_dest = "genders",
convert = function(arg)
return rsplit(arg, ",")
end,
},
}
return parse_pron_modifier(arg, parse_err, generate_obj, param_mods)
end
local function generate_audio_obj(arg)
local file, gloss
if arg:find("#") then
file, gloss = arg:match("^(.-)%s*#%s*(.*)$")
else
file, gloss = arg:match("^(.-)%s*;%s*(.*)$")
end
if not file then
file = arg
gloss = "Audio"
end
return {file = file, gloss = gloss}
end
local function parse_audio(arg, parse_err)
-- None other than qualifiers
local param_mods = {}
-- Don't split on comma because some filenames have embedded commas not followed by a space
-- (typically followed by an underscore).
return parse_pron_modifier(arg, parse_err, generate_audio_obj, param_mods, "no split on comma")
end
-- External entry point for {{ast-pr}}.
function export.show_pr(frame)
local params = {
[1] = {list = true},
["rhyme"] = {},
["hyph"] = {},
["hmp"] = {},
["audio"] = {list = true},
["pagename"] = {},
}
local parargs = frame:getParent().args
local args = require("Module:parameters").process(parargs, params)
local pagename = args.pagename or mw.title.getCurrentTitle().subpageText
-- Parse the arguments.
local respellings = #args[1] > 0 and args[1] or {"+"}
local parsed_respellings = {}
local function overall_parse_err(msg, arg, val)
error(msg .. ": " .. arg .. "= " .. val)
end
local overall_rhyme = args.rhyme and
parse_rhyme(args.rhyme, function(msg) overall_parse_err(msg, "rhyme", args.rhyme) end) or nil
local overall_hyph = args.hyph and
parse_hyph(args.hyph, function(msg) overall_parse_err(msg, "hyph", args.hyph) end) or nil
local overall_hmp = args.hmp and
parse_homophone(args.hmp, function(msg) overall_parse_err(msg, "hmp", args.hmp) end) or nil
local overall_audio
if args.audio then
overall_audio = {}
for _, audio in ipairs(args.audio) do
local parsed_audio = parse_audio(audio, function(msg) overall_parse_err(msg, "audio", audio) end)
if #parsed_audio > 1 then
error("Internal error: Saw more than one object returned from parse_audio")
end
table.insert(overall_audio, parsed_audio[1])
end
end
for i, respelling in ipairs(respellings) do
if respelling:find("<") then
local param_mods = {
pre = { overall = true },
post = { overall = true },
bullets = {
overall = true,
convert = function(arg, parse_err)
if not arg:find("^[0-9]+$") then
parse_err("Modifier 'bullets' should have a number as argument, but saw '" .. arg .. "'")
end
return tonumber(arg)
end,
},
rhyme = {
overall = true,
store = "insert-flattened",
convert = parse_rhyme,
},
hyph = {
overall = true,
store = "insert-flattened",
convert = parse_hyph,
},
hmp = {
overall = true,
store = "insert-flattened",
convert = parse_homophone,
},
audio = {
overall = true,
store = "insert-flattened",
convert = parse_audio,
},
ref = { store = "insert" },
q = { store = "insert" },
qq = { store = "insert" },
a = { store = "insert" },
aa = { store = "insert" },
}
local parsed = require(put_module).parse_inline_modifiers(respelling, {
paramname = i,
param_mods = param_mods,
generate_obj = function(term, parse_err)
return parse_respelling(term, pagename, parse_err)
end,
splitchar = ",",
outer_container = {
audio = {}, rhyme = {}, hyph = {}, hmp = {}
}
})
if not parsed.bullets then
parsed.bullets = 1
end
table.insert(parsed_respellings, parsed)
else
local termobjs = {}
local function parse_err(msg)
error(msg .. ": " .. i .. "= " .. respelling)
end
for _, term in ipairs(split_on_comma(respelling)) do
table.insert(termobjs, parse_respelling(term, pagename, parse_err))
end
table.insert(parsed_respellings, {
terms = termobjs,
audio = {},
rhyme = {},
hyph = {},
hmp = {},
bullets = 1,
})
end
end
if overall_hyph then
local hyphs = {}
for _, hyph in ipairs(overall_hyph) do
if hyph.syllabification == "+" then
hyph.syllabification = syllabify_from_spelling(pagename)
hyph.hyph = split_syllabified_spelling(hyph.syllabification)
elseif hyph.syllabification == "-" then
overall_hyph = {}
break
end
end
end
-- Loop over individual respellings, processing each.
for _, parsed in ipairs(parsed_respellings) do
parsed.pronun = generate_pronun(parsed)
local no_auto_rhyme = false
for _, term in ipairs(parsed.terms) do
if term.raw then
if not should_generate_rhyme_from_ipa(term.raw_phonemic) then
no_auto_rhyme = true
break
end
elseif not should_generate_rhyme_from_respelling(term.term) then
no_auto_rhyme = true
break
end
end
if #parsed.hyph == 0 then
if not overall_hyph and all_words_have_vowels(pagename) then
for _, term in ipairs(parsed.terms) do
if not term.raw then
local syllabification = syllabify_from_spelling(term.term)
local aligned_syll = align_syllabification_to_spelling(syllabification, pagename)
if aligned_syll then
m_table.insertIfNot(parsed.hyph, generate_hyph_obj(aligned_syll))
end
end
end
end
else
for _, hyph in ipairs(parsed.hyph) do
if hyph.syllabification == "+" then
hyph.syllabification = syllabify_from_spelling(pagename)
hyph.hyph = split_syllabified_spelling(hyph.syllabification)
elseif hyph.syllabification == "-" then
parsed.hyph = {}
break
end
end
end
-- Generate the rhymes.
local function dodialect_rhymes_from_pronun(rhyme_ret)
rhyme_ret.pronun = {}
for _, pronun in ipairs(parsed.pronun.pronun) do
-- We should have already excluded multiword terms and terms without vowels from rhyme generation (see
-- `no_auto_rhyme` below). But make sure to check that pronun.phonemic exists (it may not if raw
-- phonetic-only pronun is given).
if pronun.phonemic then
-- Count number of syllables by looking at syllable boundaries (including stress marks).
local num_syl = get_num_syl_from_phonemic(pronun.phonemic)
-- Get the rhyme by truncating everything up through the last stress mark + any following
-- consonants, and remove syllable boundary markers.
local rhyme = convert_phonemic_to_rhyme(pronun.phonemic)
local saw_already = false
for _, existing in ipairs(rhyme_ret.pronun) do
if existing.rhyme == rhyme then
saw_already = true
-- We already saw this rhyme but possibly with a different number of syllables,
-- e.g. if the user specified two pronunciations 'biología' (4 syllables) and
-- 'bi.ología' (5 syllables), both of which have the same rhyme /ia/.
m_table.insertIfNot(existing.num_syl, num_syl)
break
end
end
if not saw_already then
table.insert(rhyme_ret.pronun, {
rhyme = rhyme,
num_syl = {num_syl},
})
end
end
end
end
if #parsed.rhyme == 0 then
if overall_rhyme or no_auto_rhyme then
parsed.rhyme = nil
else
parsed.rhyme = express_all_styles(dodialect_rhymes_from_pronun)
end
else
local no_rhyme = false
for _, rhyme in ipairs(parsed.rhyme) do
if rhyme.rhyme == "-" then
no_rhyme = true
break
end
end
if no_rhyme then
parsed.rhyme = nil
else
local function this_dodialect(rhyme_ret, dialect)
return dodialect_specified_rhymes(parsed.rhyme, parsed.hyph, {parsed}, rhyme_ret)
end
parsed.rhyme = express_all_styles(this_dodialect)
end
end
end
if overall_rhyme then
local no_overall_rhyme = false
for _, orhyme in ipairs(overall_rhyme) do
if orhyme.rhyme == "-" then
no_overall_rhyme = true
break
end
end
if no_overall_rhyme then
overall_rhyme = nil
else
local all_hyphs
if overall_hyph then
all_hyphs = overall_hyph
else
all_hyphs = {}
for _, parsed in ipairs(parsed_respellings) do
for _, hyph in ipairs(parsed.hyph) do
m_table.insertIfNot(all_hyphs, hyph)
end
end
end
local function dodialect_overall_rhyme(rhyme_ret, dialect)
return dodialect_specified_rhymes(overall_rhyme, all_hyphs, parsed_respellings, rhyme_ret)
end
overall_rhyme = express_all_styles(dodialect_overall_rhyme)
end
end
-- If all sets of pronunciations have the same rhymes, display them only once at the bottom.
-- Otherwise, display rhymes beneath each set, indented.
local first_rhyme_ret
local all_rhyme_sets_eq = true
for j, parsed in ipairs(parsed_respellings) do
if j == 1 then
first_rhyme_ret = parsed.rhyme
elseif not m_table.deepEquals(first_rhyme_ret, parsed.rhyme) then
all_rhyme_sets_eq = false
break
end
end
local function format_rhyme(rhyme_ret, num_bullets)
local function format_rhyme_style(tag, expressed_style, is_first)
local pronunciations = {}
local rhymes = {}
for _, pronun in ipairs(expressed_style.pronun) do
table.insert(rhymes, pronun)
end
local data = {
lang = lang,
rhymes = rhymes,
qualifiers = tag and {tag} or nil,
force_cat = force_cat,
}
local bullet = string.rep("*", num_bullets) .. " "
local formatted = bullet .. require("Module:rhymes").format_rhymes(data)
local formatted_for_len_parts = {}
table.insert(formatted_for_len_parts, bullet .. "Rhymes: " .. (tag and "(" .. tag .. ") " or ""))
for j, pronun in ipairs(expressed_style.pronun) do
if j > 1 then
table.insert(formatted_for_len_parts, ", ")
end
if pronun.qualifiers then
table.insert(formatted_for_len_parts, "(" .. table.concat(pronun.qualifiers, ", ") .. ") ")
end
table.insert(formatted_for_len_parts, "-" .. pronun.rhyme)
end
return formatted, textual_len(table.concat(formatted_for_len_parts))
end
return format_all_styles(rhyme_ret.expressed_styles, format_rhyme_style)
end
-- If all sets of pronunciations have the same hyphenations, display them only once at the bottom.
-- Otherwise, display hyphenations beneath each set, indented.
local first_hyphs
local all_hyph_sets_eq = true
for j, parsed in ipairs(parsed_respellings) do
if j == 1 then
first_hyphs = parsed.hyph
elseif not m_table.deepEquals(first_hyphs, parsed.hyph) then
all_hyph_sets_eq = false
break
end
end
local function format_hyphenations(hyphs, num_bullets)
local hyphtext = require("Module:hyphenation").format_hyphenations { lang = lang, hyphs = hyphs, caption = "Pagpapantig" }
return string.rep("*", num_bullets) .. " " .. hyphtext
end
-- If all sets of pronunciations have the same homophones, display them only once at the bottom.
-- Otherwise, display homophones beneath each set, indented.
local first_hmps
local all_hmp_sets_eq = true
for j, parsed in ipairs(parsed_respellings) do
if j == 1 then
first_hmps = parsed.hmp
elseif not m_table.deepEquals(first_hmps, parsed.hmp) then
all_hmp_sets_eq = false
break
end
end
local function format_homophones(hmps, num_bullets)
local hmptext = require("Module:homophones").format_homophones { lang = lang, homophones = hmps }
return string.rep("*", num_bullets) .. " " .. hmptext
end
local function format_audio(audios, num_bullets)
local ret = {}
for i, audio in ipairs(audios) do
local text = require(audio_module).format_audio {
lang = lang,
file = audio.file,
caption = audio.gloss,
q = audio.q,
qq = audio.qq,
a = audio.a,
aa = audio.aa,
}
table.insert(ret, string.rep("*", num_bullets) .. " " .. text)
end
return table.concat(ret, "\n")
end
local textparts = {}
local min_num_bullets = math.huge
for j, parsed in ipairs(parsed_respellings) do
if parsed.bullets < min_num_bullets then
min_num_bullets = parsed.bullets
end
if j > 1 then
table.insert(textparts, "\n")
end
table.insert(textparts, parsed.pronun.text)
if #parsed.audio > 0 then
table.insert(textparts, "\n")
-- If only one pronunciation set, add the audio with the same number of bullets, otherwise
-- indent audio by one more bullet.
table.insert(textparts, format_audio(parsed.audio,
#parsed_respellings == 1 and parsed.bullets or parsed.bullets + 1))
end
if not all_rhyme_sets_eq and parsed.rhyme then
table.insert(textparts, "\n")
table.insert(textparts, format_rhyme(parsed.rhyme, parsed.bullets + 1))
end
if not all_hyph_sets_eq and #parsed.hyph > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_hyphenations(parsed.hyph, parsed.bullets + 1))
end
if not all_hmp_sets_eq and #parsed.hmp > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_homophones(parsed.hmp, parsed.bullets + 1))
end
end
if overall_audio and #overall_audio > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_audio(overall_audio, min_num_bullets))
end
if all_rhyme_sets_eq and first_rhyme_ret then
table.insert(textparts, "\n")
table.insert(textparts, format_rhyme(first_rhyme_ret, min_num_bullets))
end
if overall_rhyme then
table.insert(textparts, "\n")
table.insert(textparts, format_rhyme(overall_rhyme, min_num_bullets))
end
if all_hyph_sets_eq and #first_hyphs > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_hyphenations(first_hyphs, min_num_bullets))
end
if overall_hyph and #overall_hyph > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_hyphenations(overall_hyph, min_num_bullets))
end
if all_hmp_sets_eq and #first_hmps > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_homophones(first_hmps, min_num_bullets))
end
if overall_hmp and #overall_hmp > 0 then
table.insert(textparts, "\n")
table.insert(textparts, format_homophones(overall_hmp, min_num_bullets))
end
return table.concat(textparts)
end
-- External entry point for {{ast-IPA}}.
function export.show(frame)
local params = {
[1] = {},
["pre"] = {},
["post"] = {},
["ref"] = {},
["style"] = {},
["bullets"] = {type = "number", default = 1},
}
local parargs = frame:getParent().args
local args = require(parameters_module).process(parargs, params)
local text = args[1] or mw.title.getCurrentTitle().text
args.terms = {{term = text}}
local ret = generate_pronun(args)
return ret.text
end
return export
7umwdkhncukoxvl50pzltt0teqmzy02
Module:ca-headword
828
37207
178549
176070
2026-10-29T01:27:45Z
GinawaSaHapon
3989
178549
Scribunto
text/plain
local export = {}
local pos_functions = {}
local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local require_when_needed = require("Module:utilities/require when needed")
local m_table = require("Module:table")
local com = require("Module:ca-common")
local ca_IPA_module = "Module:ca-IPA"
local ca_verb_module = "Module:ca-verb"
local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local inflection_utilities_module = "Module:inflection utilities"
local parse_utilities_module = "Module:parse utilities"
local romut_module = "Module:romance utilities"
local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local m_string_utilities = require_when_needed("Module:string utilities")
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")
local lang = require("Module:languages").getByCode("ca")
local langname = lang:getCanonicalName()
local list_to_text = mw.text.listToText
local insert = table.insert
local concat = table.concat
local rfind = m_string_utilities.find
local rmatch = m_string_utilities.match
local rsplit = m_string_utilities.split
local usub = m_string_utilities.sub
local rsub = com.rsub
local function track(page)
require("Module:debug/track")("ca-headword/" .. page)
return true
end
local list_param = {list = true, disallow_holes = true}
local boolean_param = {type = "boolean"}
-----------------------------------------------------------------------------------------
-- Main entry point --
-----------------------------------------------------------------------------------------
-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
local poscat = frame.args[1] or error("Part of speech has not been specified. Please pass parameter 1 to the module invocation.")
local params = {
["head"] = list_param,
["id"] = true,
["splithyph"] = boolean_param,
["nolinkhead"] = boolean_param,
["json"] = boolean_param,
["pagename"] = true, -- for testing
}
if pos_functions[poscat] then
for key, val in pairs(pos_functions[poscat].params) do
params[key] = val
end
end
local args = require("Module:parameters").process(frame:getParent().args, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
local user_specified_heads = args.head
local heads = user_specified_heads
if args.nolinkhead then
if #heads == 0 then
heads = {pagename}
end
else
local romut = require(romut_module)
local auto_linked_head = romut.add_links_to_multiword_term(pagename, args.splithyph)
if #heads == 0 then
heads = {auto_linked_head}
else
for i, head in ipairs(heads) do
if head:find("^~") then
head = romut.apply_link_modifiers(auto_linked_head, usub(head, 2))
heads[i] = head
end
if head == auto_linked_head then
track("redundant-head")
end
end
end
end
local data = {
lang = lang,
pos_category = pos_functions[poscat] and pos_functions[poscat].pos_category or poscat,
categories = {},
heads = heads,
user_specified_heads = user_specified_heads,
no_redundant_head_cat = #user_specified_heads == 0,
genders = {},
inflections = {},
pagename = pagename,
id = args.id,
force_cat_output = force_cat,
checkredlinks = pos_functions[poscat] and pos_functions[poscat].redlink_pos or true,
}
if pagename:find("^%-") and poscat ~= "suffix forms" then
data.is_suffix = true
data.pos_category = "suffixes"
data.checkredlinks = true
local singular_poscat = poscat
insert(data.categories, langname .. " na hulaping gumagawa ng " .. singular_poscat)
insert(data.inflections, {label = "hulaping gumagawa ng " .. singular_poscat})
end
if pos_functions[poscat] then
pos_functions[poscat].func(args, data)
end
if args.json then
return require("Module:JSON").toJSON(data)
end
local post_note = data.post_note and "; " .. data.post_note or ""
return require("Module:headword").full_headword(data) .. post_note
end
-----------------------------------------------------------------------------------------
-- Utility functions --
-----------------------------------------------------------------------------------------
local function replace_hash_with_lemma(term, lemma)
-- If there is a % sign in the lemma, we have to replace it with %% so it doesn't get interpreted as a capture replace
-- expression.
lemma = lemma:gsub("%%", "%%%%")
-- Assign to a variable to discard second return value.
term = term:gsub("#", lemma)
return term
end
-- Parse and insert an inflection not requiring additional processing into `data.inflections`. The raw arguments come
-- from `args[field]`, which is parsed for inline modifiers. `label` is the label that the inflections are given;
-- `accel` is the accelerator form, or nil.
local function parse_and_insert_inflection(data, args, field, label, accel)
m_headword_utilities.parse_and_insert_inflection {
headdata = data,
forms = args[field],
paramname = field,
splitchar = ",",
label = label,
accel = accel and {form = accel} or nil,
}
end
-- Insert default plurals generated when a given plural had the value of + and default plurals were fetched as a result.
-- `plobj` is the parsed object whose `term` field is "+". `defpls` is the list of default plurals. `dest` is the list
-- into which the plurals are inserted (which inherit their qualifiers and labels from `plobj`).
local function insert_defpls(defpls, plobj, dest)
if not defpls then
-- Happens e.g. with [[S.A.]] where the default plural algorithm returns nothing.
return
end
if #defpls == 1 then
plobj.term = defpls[1]
insert(dest, plobj)
else
for _, defpl in ipairs(defpls) do
local newplobj = m_table.shallowCopy(plobj)
newplobj.term = defpl
insert(dest, newplobj)
end
end
end
-----------------------------------------------------------------------------------------
-- Adjectives --
-----------------------------------------------------------------------------------------
local function do_adjective(args, data, is_superlative)
local feminines = {}
local masculine_plurals = {}
local feminine_plurals = {}
-- Use "participle" not "past participle" for categories such as 'invariable paticiples'
local category_plpos = data.checkredlinks
if category_plpos == true then
category_plpos = data.pos_category
end
local category_pos = category_plpos
if args.sp then
local romut = require(romut_module)
if not romut.allowed_special_indicators[args.sp] then
local indicators = {}
for indic, _ in pairs(romut.allowed_special_indicators) do
insert(indicators, "'" .. indic .. "'")
end
table.sort(indicators)
error("Special inflection indicator beginning can only be " ..
list_to_text(indicators) .. ": " .. args.sp)
end
end
local lemma = data.pagename
local function fetch_inflections(field)
local retval = m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[field],
splitchar = ",",
}
if not retval[1] then
return {{term = "+"}}
end
return retval
end
local function insert_inflection(terms, label, accel)
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
}
end
if args.f[1] == "ind" or args.f[1] == "inv" then
-- invariable adjective
insert(data.inflections, {label = glossary_link("di-nababago")})
insert(data.categories, langname .. " na di-mababanghay na " .. category_plpos)
if args.sp or args.f[2] or args.pl[1] or args.mpl[1] or args.fpl[1] then
error("Can't specify inflections with an invariable " .. category_pos)
end
elseif args.fonly then
-- feminine-only
if args.f[1] then
error("Can't specify explicit feminines with feminine-only " .. category_pos)
end
if args.pl[1] then
error("Can't specify explicit plurals with feminine-only " .. category_pos .. ", use fpl=")
end
if args.mpl[1] then
error("Can't specify explicit masculine plurals with feminine-only " .. category_pos)
end
local argsfpl = fetch_inflections("fpl")
for _, fpl in ipairs(argsfpl) do
if fpl.term == "+" then
-- Generate default feminine plural.
local defpls = com.make_plural(lemma, "f", args.sp)
if not defpls then
error("Unable to generate default plural of '" .. lemma .. "'")
end
insert_defpls(defpls, fpl, feminine_plurals)
else
fpl.term = replace_hash_with_lemma(fpl.term, lemma)
insert(feminine_plurals, fpl)
end
end
insert(data.inflections, {label = "pambabae lamang"})
insert_inflection(feminine_plurals, "pambabae maramihan", "f|p")
else
-- Gather feminines.
for _, f in ipairs(fetch_inflections("f")) do
if f.term == "mf" then
f.term = lemma
elseif f.term == "+" then
-- Generate default feminine.
f.term = com.make_feminine(lemma, args.sp)
else
f.term = replace_hash_with_lemma(f.term, lemma)
end
insert(feminines, f)
end
local fem_like_lemma = #feminines == 1 and feminines[1].term == lemma and
not m_headword_utilities.termobj_has_qualifiers_or_labels(feminines[1])
if fem_like_lemma then
insert(data.categories, langname .. " na di-tiyak na " .. category_plpos)
end
local mpl_field = "mpl"
local fpl_field = "fpl"
if args.pl[1] then
if args.mpl[1] or args.fpl[1] then
error("Can't specify both pl= and mpl=/fpl=")
end
mpl_field = "pl"
fpl_field = "pl"
end
local argsmpl = fetch_inflections(mpl_field)
local argsfpl = fetch_inflections(fpl_field)
for _, mpl in ipairs(argsmpl) do
if mpl.term == "+" then
-- Generate default masculine plural.
local defpls
-- First, some special hacks based on the feminine singular.
if not fem_like_lemma and not args.sp and not lemma:find(" ") then
for _, f in ipairs(feminines) do
if f.term:find("ssa$") then
-- If the feminine ends in -ssa, assume that the -ss- is also in the
-- masculine plural form
defpls = {rsub(f.term, "a$", "os")}
break
elseif f.term == lemma .. "na" then
defpls = {lemma .. "ns"}
break
elseif lemma:find("ig$") and f.term:find("ja$") then
-- Adjectives in -ig have two masculine plural forms, one derived from
-- the m.sg. and the other derived from the f.sg.
defpls = {lemma .. "s", rsub(f.term, "ja$", "jos")}
break
end
end
end
defpls = defpls or com.make_plural(lemma, "m", args.sp)
if not defpls then
error("Unable to generate default plural of '" .. lemma .. "'")
end
insert_defpls(defpls, mpl, masculine_plurals)
else
mpl.term = replace_hash_with_lemma(mpl.term, lemma)
insert(masculine_plurals, mpl)
end
end
for _, fpl in ipairs(argsfpl) do
if fpl.term == "+" then
-- First, some special hacks based on the feminine singular.
if fem_like_lemma and not args.sp and not lemma:find(" ") and lemma:find("[çx]$") then
-- Adjectives ending in -ç or -x behave as mf-type in the singular, but
-- regular type in the plural.
local defpls = com.make_plural(lemma .. "a", "f")
if not defpls then
error("Unable to generate default plural of '" .. lemma .. "a'")
end
insert_defpls(defpls, fpl, feminine_plurals)
else
for _, f in ipairs(feminines) do
-- Generate default feminine plural; f is a table.
local defpls = com.make_plural(f.term, "f", args.sp)
if not defpls then
error("Unable to generate default plural of '" .. f.term .. "'")
end
for _, defpl in ipairs(defpls) do
local fplobj = m_table.shallowCopy(fpl)
fplobj.term = defpl
m_headword_utilities.combine_termobj_qualifiers_labels(fplobj, f)
insert(feminine_plurals, fplobj)
end
end
end
else
fpl.term = replace_hash_with_lemma(fpl.term, lemma)
insert(feminine_plurals, fpl)
end
end
local fem_pl_like_masc_pl = masculine_plurals[1] and feminine_plurals[1] and
m_table.deepEquals(masculine_plurals, feminine_plurals)
local masc_pl_like_lemma = #masculine_plurals == 1 and masculine_plurals[1].term == lemma and
not m_headword_utilities.termobj_has_qualifiers_or_labels(masculine_plurals[1])
if fem_like_lemma and fem_pl_like_masc_pl and masc_pl_like_lemma then
-- actually invariable
insert(data.inflections, {label = glossary_link("di-nababago")})
insert(data.categories, langname .. " na di-mababanghay na " .. category_plpos)
else
-- Make sure there are feminines given and not same as lemma.
if not fem_like_lemma then
insert_inflection(feminines, "pambabae", "f|s")
elseif args.gneut then
data.genders = {"gneut"}
else
data.genders = {"mf"}
end
if fem_pl_like_masc_pl then
if args.gneut then
insert_inflection(masculine_plurals, "maramihan", "p")
else
insert_inflection(masculine_plurals, "panlalaki at pambabae maramihan", "p")
end
else
insert_inflection(masculine_plurals, "panlalaki maramihan", "m|p")
insert_inflection(feminine_plurals, "pambabae maramihan", "f|p")
end
end
end
parse_and_insert_inflection(data, args, "comp", "pahambing")
parse_and_insert_inflection(data, args, "sup", "pasukdol")
parse_and_insert_inflection(data, args, "dim", "pampaliit")
parse_and_insert_inflection(data, args, "aug", "pampalaki")
if args.irreg and is_superlative then
insert(data.categories, langname .. " na di-regular na pasukdol na " .. category_plpos)
end
end
local function get_adjective_params(adjtype)
local params = {
["sp"] = true, -- special indicator: "first", "first-last", etc.
["f"] = list_param, --feminine form(s)
[1] = {alias_of = "f", list = false},
["pl"] = list_param, --plural override(s)
["mpl"] = list_param, --masculine plural override(s)
["fpl"] = list_param, --feminine plural override(s)
}
if adjtype == "base" then
params["comp"] = list_param --comparative(s)
params["sup"] = list_param --superlative(s)
params["dim"] = list_param --diminutive(s)
params["aug"] = list_param --augmentative(s)
params["fonly"] = boolean_param -- feminine only
params["hascomp"] = {} -- has comparative
end
if adjtype == "sup" then
params["irreg"] = boolean_param
end
return params
end
-- Display additional inflection information for an adjective
pos_functions["adjectives"] = {
params = get_adjective_params("base"),
func = do_adjective,
}
pos_functions["past participles"] = {
params = get_adjective_params("part"),
func = do_adjective,
redlink_pos = "participles",
}
pos_functions["determiners"] = {
params = get_adjective_params("det"),
func = do_adjective,
}
pos_functions["pronouns"] = {
params = get_adjective_params("pron"),
func = do_adjective,
}
-----------------------------------------------------------------------------------------
-- Nouns --
-----------------------------------------------------------------------------------------
local allowed_genders = m_table.listToSet(
{"m", "f", "mf", "mfbysense", "mfequiv", "gneut", "n", "m-p", "f-p", "mf-p", "mfbysense-p", "mfequiv-p", "gneut-p", "n-p", "?", "?-p"}
)
local function validate_genders(genders)
for _, g in ipairs(genders) do
if type(g) == "table" then
g = g.spec
end
if not allowed_genders[g] then
error("Unrecognized gender: " .. g)
end
end
end
local function do_noun(args, data, is_proper)
local is_plurale_tantum = false
local has_singular = false
local category_plpos = data.checkredlinks
if category_plpos == true then
category_plpos = data.pos_category
end
local category_pos = m_en_utilities.singularize(category_plpos)
validate_genders(args[1])
data.genders = args[1]
local saw_m = false
local saw_f = false
local saw_gneut = false
local gender_for_irreg_ending, gender_for_default_plural
-- Check for specific genders and pluralia tantum.
for _, g in ipairs(args[1]) do
if type(g) == "table" then
g = g.spec
end
if g:find("-p$") then
is_plurale_tantum = true
else
has_singular = true
if g == "m" or g == "mf" or g == "mfbysense" then
saw_m = true
end
if g == "f" or g == "mf" or g == "mfbysense" then
saw_f = true
end
if g == "gneut" then
saw_gneut = true
end
end
end
if saw_m and saw_f then
gender_for_irreg_ending = "mf"
elseif saw_f then
gender_for_irreg_ending = "f"
else
gender_for_irreg_ending = "m"
end
gender_for_default_plural =
saw_gneut and "gneut" or gender_for_irreg_ending == "mf" and "m" or gender_for_irreg_ending
local lemma = data.pagename
-- Plural
local plurals = {}
local function insert_noun_inflection(terms, label, accel)
m_headword_utilities.insert_inflection {
headdata = data,
terms = terms,
label = label,
accel = accel and {form = accel} or nil,
}
end
if is_plurale_tantum and not has_singular then
if args[2][1] then
error("Can't specify plurals of plurale tantum " .. category_pos)
end
insert(data.inflections, {label = glossary_link("maramihan lamang")})
else
plurals = m_headword_utilities.parse_term_list_with_modifiers {
paramname = {2, "pl"},
forms = args[2],
splitchar = ",",
}
-- Check for special plural signals
local mode = nil
local pl1 = plurals[1]
if pl1 and #pl1.term == 1 then
mode = pl1.term
if mode == "?" or mode == "!" or mode == "-" or mode == "~" then
pl1.term = nil
if next(pl1) then
error(("Can't specify inline modifiers with plural code '%s'"):format(mode))
end
table.remove(plurals, 1) -- Remove the mode parameter
elseif mode ~= "+" and mode ~= "#" then
error(("Unexpected plural code '%s'"):format(mode))
end
end
if is_plurale_tantum then
-- both singular and plural
insert(data.inflections, {label = "minsan " .. glossary_link("maramihan lamang") .. ", pabago-bago"})
end
if mode == "?" then
-- Plural is unknown
insert(data.categories, langname .. " na " .. category_plpos .. " na may di-alam o di-tiyak na maramihan")
elseif mode == "!" then
-- Plural is not attested
insert(data.inflections, {label = "di-kumpirmado ang maramihan"})
insert(data.categories, langname .. " na " .. category_plpos .. " na may di-kumpirmadong maramihan")
if plurals[1] then
error("Can't specify any plurals along with unattested plural code '!'")
end
elseif mode == "-" then
-- Uncountable noun; may occasionally have a plural
insert(data.categories, langname .. " na di-mabibilang na " .. category_plpos)
-- If plural forms were given explicitly, then show "usually"
if plurals[1] then
insert(data.inflections, {label = "madalas " .. glossary_link("di-mabibilang")})
insert(data.categories, langname .. " na mabibilang na " .. category_plpos)
else
insert(data.inflections, {label = glossary_link("di-mabibilang")})
end
else
-- Countable or mixed countable/uncountable
if not plurals[1] and not is_proper then
plurals[1] = {term = "+"}
end
if mode == "~" then
-- Mixed countable/uncountable noun, always has a plural
insert(data.inflections, {label = glossary_link("mabibilang") .. " at " .. glossary_link("di-mabibilang")})
insert(data.categories, langname .. " na di-mabibilang na " .. category_plpos)
insert(data.categories, langname .. " na mabibilang na " .. category_plpos)
elseif plurals[1] then
-- Countable nouns
insert(data.categories, langname .. " na mabibilang na " .. category_plpos)
else
-- Uncountable nouns
insert(data.categories, langname .. " na mabibilang na " .. category_plpos)
end
end
-- Gather plurals, handling requests for default plurals.
local has_default_or_hash = false
for _, pl in ipairs(plurals) do
if pl.term:find("^%+") or pl.term:find("#") then
has_default_or_hash = true
break
end
end
if has_default_or_hash then
local newpls = {}
for _, pl in ipairs(plurals) do
if pl.term == "+" then
local default_pls = com.make_plural(lemma, gender_for_default_plural)
insert_defpls(default_pls, pl, newpls)
elseif pl.term:find("^%+") then
pl.term = require(romut_module).get_special_indicator(pl.term)
local default_pls = com.make_plural(lemma, gender_for_default_plural, pl.term)
insert_defpls(default_pls, pl, newpls)
else
pl.term = replace_hash_with_lemma(pl.term, lemma)
insert(newpls, pl)
end
end
plurals = newpls
end
local pl1 = plurals[1]
if pl1 and not plurals[2] and pl1.term == lemma then
insert(data.inflections, {label = glossary_link("di-mababago"),
q = pl1.q, qq = pl1.qq, l = pl1.l, ll = pl1.ll, refs = pl1.refs
})
insert(data.categories, langname .. " na di-mababanghay na " .. category_plpos)
else
insert_noun_inflection(plurals, "maramihan", "p")
end
if plurals[2] then
insert(data.categories, langname .. " na " .. category_plpos .. " na may maraming maramihan")
end
end
-- Gather masculines/feminines. For each one, generate the corresponding plural. `field` is the name of the field
-- containing the masculine or feminine forms (normally "m" or "f"); `inflect` is a function of one or two arguments
-- to generate the default masculine or feminine from the lemma (the arguments are the lemma and optionally a
-- "special" flag to indicate how to handle multiword lemmas, and the function is normally make_feminine or
-- make_masculine from [[Module:ca-common]]); and `default_plurals` is a list into which the corresponding default
-- plurals of the gathered or generated masculine or feminine forms are stored.
local function handle_mf(field, inflect, default_plurals)
local function call_inflect(special)
if inflect then
-- Generate default feminine.
return inflect(lemma, special)
else
-- FIXME
error("Can't generate default masculine currently")
end
end
local mfs = m_headword_utilities.parse_term_list_with_modifiers {
paramname = field,
forms = args[field],
splitchar = ",",
frob = function(term)
if term == "+" then
-- Generate default masculine/feminine.
term = call_inflect()
else
term = replace_hash_with_lemma(term, lemma)
end
local special = require(romut_module).get_special_indicator(term)
if special then
term = call_inflect(special)
end
return term
end
}
for _, mf in ipairs(mfs) do
local mfpls = com.make_plural(mf.term, gender, special)
if mfpls then
for _, mfpl in ipairs(mfpls) do
local plobj = m_table.shallowCopy(mf)
plobj.term = mfpl
-- Add an accelerator for each masculine/feminine plural whose lemma
-- is the corresponding singular, so that the accelerated entry
-- that is generated has a definition that looks like
-- # {{plural of|ca|MFSING}}
plobj.accel = {form = "p", lemma = mf.term}
table.insert(default_plurals, plobj)
end
end
end
return mfs
end
local feminine_plurals = {}
local feminines = handle_mf("f", com.make_feminine, feminine_plurals)
local masculine_plurals = {}
local masculines = handle_mf("m", com.make_masculine, masculine_plurals)
local function handle_mf_plural(mfplfield, default_plurals, singulars)
local mfpl = m_headword_utilities.parse_term_list_with_modifiers {
paramname = mfplfield,
forms = args[mfplfield],
splitchar = ",",
}
local new_mfpls = {}
local saw_plus
for i, mfpl in ipairs(mfpl) do
local accel
if #mfpl == #singulars then
-- If same number of overriding masculine/feminine plurals as singulars, assume each plural goes with
-- the corresponding singular and use each corresponding singular as the lemma in the accelerator. The
-- generated entry will have
-- # {{plural of|ca|SINGULAR}}
-- as the definition.
accel = {form = "p", lemma = singulars[i].term}
else
accel = nil
end
if mfpl.term == "+" then
-- We should never see + twice. If we do, it will lead to problems since we overwrite the values of
-- default_plurals the first time around.
if saw_plus then
error(("Saw + twice when handling %s="):format(mfplfield))
end
saw_plus = true
if not default_plurals[1] then
-- FIXME: Can this happen? Not in corresponding Spanish code and the old Portuguese code tried to
-- handle this condition by generating the default plural from the lemma.
error("Internal error: Something wrong, no generated default m/f plurals at this stage")
end
for _, defpl in ipairs(default_plurals) do
-- defpl is already a table and has an accel field
m_headword_utilities.combine_termobj_qualifiers_labels(defpl, mfpl)
insert(new_mfpls, defpl)
end
elseif mfpl.term:find("^%+") then
mfpl.term = require(romut_module).get_special_indicator(mfpl.term)
for _, mf in ipairs(singulars) do
local default_mfpls = com.make_plural(mf.term, gender, mfpl.term)
for _, defp in ipairs(default_mfpls) do
local mfplobj = m_table.shallowCopy(mfpl)
mfplobj.term = defp
mfplobj.accel = accel
m_headword_utilities.combine_termobj_qualifiers_labels(mfplobj, mf)
insert(new_mfpls, mfplobj)
end
end
else
mfpl.accel = accel
mfpl.term = replace_hash_with_lemma(mfpl.term, lemma)
insert(new_mfpls, mfpl)
end
end
return new_mfpls
end
if args.fpl[1] then
-- Override any existing feminine plurals.
feminine_plurals = handle_mf_plural("fpl", feminine_plurals, feminines)
end
if args.mpl[1] then
-- Override any existing masculine plurals.
masculine_plurals = handle_mf_plural("mpl", masculine_plurals, masculines)
end
local function parse_and_insert_noun_inflection(field, label, accel)
parse_and_insert_inflection(data, args, field, label, accel)
end
insert_noun_inflection(feminines, "pambabae", "f")
insert_noun_inflection(feminine_plurals, "pambabae maramihan")
insert_noun_inflection(masculines, "panlalaki")
insert_noun_inflection(masculine_plurals, "panlalaki maramihan")
parse_and_insert_noun_inflection("dim", "pampaliit")
parse_and_insert_noun_inflection("aug", "pampalaki")
parse_and_insert_noun_inflection("pej", "pampanira")
parse_and_insert_noun_inflection("dem", "ngalang-lipi")
parse_and_insert_noun_inflection("fdem", "pambabae ngalang-lipi")
-- Is this a noun with an unexpected ending (for its gender)?
-- Only check if the term is one word (there are no spaces in the term).
local irreg_gender_lemma = rsub(lemma, " .*", "") -- only look at first word
if (gender_for_irreg_ending == "m" or gender_for_irreg_ending == "mf") and irreg_gender_lemma:find("a$") then
insert(data.categories, langname .. " na panlalaking " .. category_plpos .. " na nagtatapos sa -a")
elseif (gender_for_irreg_ending == "f" or gender_for_irreg_ending == "mf") and not (
irreg_gender_lemma:find("a$") or irreg_gender_lemma:find("ió$") or irreg_gender_lemma:find("tat$") or
irreg_gender_lemma:find("tud$") or irreg_gender_lemma:find("[dt]riu$")) then
insert(data.categories, langname .. " na pambabaeng " .. category_plpos .. " na walang pambabaeng dulo")
end
end
local function get_noun_params(is_proper)
return {
[1] = {list = "g", disallow_holes = true, required = not is_proper, default = "?", type = "genders",
flatten = true}, -- gender(s)
[2] = {list = "pl", disallow_holes = true}, --plural override(s)
["f"] = list_param, --feminine form(s)
["m"] = list_param, --masculine form(s)
["fpl"] = list_param, --feminine plural override(s)
["mpl"] = list_param, --masculine plural override(s)
["dim"] = list_param, --diminutive(s)
["aug"] = list_param, --diminutive(s)
["pej"] = list_param, --pejorative(s)
["dem"] = list_param, --demonym(s)
["fdem"] = list_param, --female demonym(s)
}
end
pos_functions["nouns"] = {
params = get_noun_params(),
func = do_noun,
}
pos_functions["proper nouns"] = {
params = get_noun_params("is proper"),
func = function(args, data)
do_noun(args, data, "is proper")
end,
}
-----------------------------------------------------------------------------------------
-- Verbs --
-----------------------------------------------------------------------------------------
pos_functions["verbs"] = {
params = {
[1] = true,
["pres"] = list_param, --present
["pres_qual"] = {list = "pres\1_qual", allow_holes = true},
["pres3s"] = list_param, --third-singular present
["pres3s_qual"] = {list = "pres3s\1_qual", allow_holes = true},
["pret"] = list_param, --preterite
["pret_qual"] = {list = "pret\1_qual", allow_holes = true},
["part"] = list_param, --participle
["part_qual"] = {list = "part\1_qual", allow_holes = true},
["short_part"] = list_param, --short participle
["short_part_qual"] = {list = "short_part\1_qual", allow_holes = true},
["noautolinktext"] = boolean_param,
["noautolinkverb"] = boolean_param,
["attn"] = boolean_param,
["pres_1_sg"] = true, -- accept any ignore old-style param
["past_part"] = true, -- accept any ignore old-style param
["root"] = true, -- FIXME: Implement root-stressed vowel quality
},
func = function(args, data, tracking_categories, frame)
local preses, preses_3s, prets, parts, short_parts
if args.attn then
insert(tracking_categories, "Requests for attention concerning " .. langname)
return
end
local ca_verb = require(ca_verb_module)
local alternant_multiword_spec = ca_verb.do_generate_forms(args, "ca-verb", data.heads[1])
local specforms = alternant_multiword_spec.forms
local function slot_exists(slot)
return specforms[slot] and #specforms[slot] > 0
end
local function do_finite(slot_tense, label_tense)
-- Use pres_3s if it exists and pres_1s doesn't exist (e.g. impersonal verbs); similarly for pres_3p (only3p verbs);
-- but fall back to pres_1s if neither pres_1s nor pres_3s nor pres_3p exist (e.g. [[empedernir]]).
local has_1s = slot_exists(slot_tense .. "_1s")
local has_3s = slot_exists(slot_tense .. "_3s")
local has_3p = slot_exists(slot_tense .. "_3p")
if has_1s or (not has_3s and not has_3p) then
return {
slot = slot_tense .. "_1s",
label = ("unang panauhan na pang-isahang %s"):format(label_tense),
}, true
elseif has_3s then
return {
slot = slot_tense .. "_3s",
label = ("ikatlong panauhan na pang-isahang %s"):format(label_tense),
}, false
else
return {
slot = slot_tense .. "_3p",
label = ("ikatlong panauhan na pangmaramihang %s"):format(label_tense),
}, false
end
end
local did_pres_1s
preses, did_pres_1s = do_finite("pres", "pangkasalukuyan")
preses_3s = {
slot = "pres_3s",
label = "ikatlong panauhan na pang-isahang pangkasalukuyan",
}
prets = do_finite("pret", "di-pangnakaraan")
parts = {
slot = "pp_ms",
label = "pandiwaring pangnakaraan",
}
short_parts = {
slot = "short_pp_ms",
label = "maikling pandiwaring pangnakaraan",
}
if args.pres[1] or args.pres3s[1] or args.pret[1] or args.part[1] or args.short_part[1] then
track("verb-old-multiarg")
end
local function strip_brackets(qualifiers)
if not qualifiers then
return nil
end
local stripped_qualifiers = {}
for _, qualifier in ipairs(qualifiers) do
local stripped_qualifier = qualifier:match("^%[(.*)%]$")
if not stripped_qualifier then
error("Internal error: Qualifier should be surrounded by brackets at this stage: " .. qualifier)
end
insert(stripped_qualifiers, stripped_qualifier)
end
return stripped_qualifiers
end
local function do_verb_form(args, qualifiers, slot_desc, skip_if_empty)
local forms
local to_insert
if #args == 0 then
forms = specforms[slot_desc.slot]
if not forms or #forms == 0 then
if skip_if_empty then
return
end
forms = {{form = "-"}}
end
elseif #args == 1 and args[1] == "-" then
forms = {{form = "-"}}
else
forms = {}
for i, arg in ipairs(args) do
local qual = qualifiers[i]
if qual then
-- FIXME: It's annoying we have to add brackets and strip them out later. The inflection
-- code adds all footnotes with brackets around them; we should change this.
qual = {"[" .. qual .. "]"}
end
local form = arg
if not args.noautolinkverb then
-- [[Module:inflection utilities]] already loaded by [[Module:ca-verb]]
form = require(inflection_utilities_module).add_links(form)
end
insert(forms, {form = form, footnotes = qual})
end
end
if forms[1].form == "-" then
to_insert = {label = "walang " .. slot_desc.label}
else
local into_table = {label = slot_desc.label}
for _, form in ipairs(forms) do
local qualifiers = strip_brackets(form.footnotes)
-- Strip redundant brackets surrounding entire form. These may get generated e.g.
-- if we use the angle bracket notation with a single word.
local stripped_form = rmatch(form.form, "^%[%[([^%[%]]*)%]%]$") or form.form
-- Don't include accelerators if brackets remain in form, as the result will be wrong.
-- FIXME: For now, don't include accelerators. We should use the new {{ca-verb form of}}.
-- local this_accel = not stripped_form:find("%[%[") and accel or nil
local this_accel = nil
insert(into_table, {term = stripped_form, q = qualifiers, accel = this_accel})
end
to_insert = into_table
end
insert(data.inflections, to_insert)
end
local skip_pres_if_empty
if alternant_multiword_spec.no_pres1_and_sub then
insert(data.inflections, {label = "walang unang panauhan na pang-isahang pangkasalukuyan"})
insert(data.inflections, {label = "walang pangkasalukuyang pasakali"})
end
if alternant_multiword_spec.no_pres_stressed then
insert(data.inflections, {label = "walang nakadiin na pangkasalukuyang paturol o pasakali"})
skip_pres_if_empty = true
end
if alternant_multiword_spec.only3s then
insert(data.inflections, {label = glossary_link("di-panao")})
elseif alternant_multiword_spec.only3sp then
insert(data.inflections, {label = "ikatlong panauhan lamang"})
elseif alternant_multiword_spec.only3p then
insert(data.inflections, {label = "ikatlong panauhan na pangmaramihan lamang"})
end
local has_vowel_alt
if alternant_multiword_spec.vowel_alt then
for _, vowel_alt in ipairs(alternant_multiword_spec.vowel_alt) do
if vowel_alt ~= "+" and vowel_alt ~= "í" and vowel_alt ~= "ú" then
has_vowel_alt = true
break
end
end
end
do_verb_form(args.pres, args.pres_qual, preses, skip_pres_if_empty)
-- We want to include both the pres_1s and pres_3s if there is a vowel alternation in the present singular. But we
-- don't want to redundantly include the pres_3s if we already included it.
if did_pres_1s and has_vowel_alt then
do_verb_form(args.pres3s, args.pres3s_qual, preses_3s, skip_pres_if_empty)
end
do_verb_form(args.pret, args.pret_qual, prets)
do_verb_form(args.part, args.part_qual, parts)
do_verb_form(args.short_part, args.short_part_qual, short_parts, "skip if empty")
-- Add categories.
for _, cat in ipairs(alternant_multiword_spec.categories) do
insert(data.categories, cat)
end
-- If the user didn't explicitly specify head=, or specified exactly one head (not 2+) and we were able to
-- incorporate any links in that head into the 1= specification, use the infinitive generated by
-- [[Module:ca-verb]] in place of the user-specified or auto-generated head. This was copied from
-- [[Module:it-headword]], where doing this gets accents marked on the verb(s). We don't have accents marked on
-- the verb but by doing this we do get any footnotes on the infinitive propagated here. Don't do this if the
-- user gave multiple heads or gave a head with a multiword-linked verbal expression such as Italian
-- '[[dare esca]] [[al]] [[fuoco]]' (FIXME: give Catalan equivalent).
if #data.user_specified_heads == 0 or (
#data.user_specified_heads == 1 and alternant_multiword_spec.incorporated_headword_head_into_lemma
) then
data.heads = {}
for _, lemma_obj in ipairs(alternant_multiword_spec.forms.infinitive_linked) do
local quals, refs = require(inflection_utilities_module).
convert_footnotes_to_qualifiers_and_references(lemma_obj.footnotes)
insert(data.heads, {term = lemma_obj.form, q = quals, refs = refs})
end
end
if args.root then
local m_ca_IPA = require(ca_IPA_module)
local parsed_respellings = {}
local function set_parsed_respelling(dialect, parsed)
-- Validate the individual root vowel specs.
for _, termobj in ipairs(parsed.terms) do
if not rfind(termobj.words[1].term, "^" .. m_ca_IPA.mid_vowel_hint_c .. "$") then
error(("Root vowel spec '%s' should be one of the vowels %s"):format(
termobj.words[1].term, m_ca_IPA.mid_vowel_hints))
end
end
if not dialect then
for _, dial in ipairs(m_ca_IPA.dialects) do
-- Need to clone as we destructively modify each one later with the pronun.
parsed_respellings[dial] = m_table.deepCopy(parsed)
end
elseif m_ca_IPA.dialect_groups[dialect] then
for _, dial in ipairs(m_ca_IPA.dialect_groups[dialect]) do
-- Need to clone as we destructively modify each one later with the pronun.
parsed_respellings[dial] = m_table.deepCopy(parsed)
end
else
parsed_respellings[dialect] = parsed
end
end
local function check_dialect_or_dialect_group(dialect)
if not m_table.contains(m_ca_IPA.dialects, dialect) and not
m_ca_IPA.dialect_groups[dialect] then
local dialect_list = {}
for _, dial in ipairs(m_ca_IPA.dialects) do
insert(dialect_list, "'" .. dial .. "'")
end
dialect_list = list_to_text(dialect_list, nil, " o ")
local dialect_group_list = {}
for dialect_group, _ in pairs(m_ca_IPA.dialect_groups) do
insert(dialect_group_list, "'" .. dialect_group .. "'")
end
dialect_group_list = list_to_text(dialect_group_list, nil, " o ")
error(("Unrecognized dialect '%s': Should be a dialect %s or a dialect group %s"):format(
dialect, dialect_list, dialect_group_list))
end
end
-- Parse the root vowel specs.
if args.root:find("[<%[]") then
local put = require(parse_utilities_module)
-- Parse balanced segment runs involving either [...] (substitution notation) or <...> (inline
-- modifiers). We do this because we don't want commas or semicolons inside of square or angle brackets
-- to count as respelling delimiters. However, we need to rejoin square-bracketed segments with nearby
-- ones after splitting alternating runs on comma and semicolon.
local segments = put.parse_multi_delimiter_balanced_segment_run(args.root, {{"<", ">"}, {"[", "]"}})
local semicolon_separated_groups = put.split_alternating_runs(segments, "%s*;%s*")
for _, group in ipairs(semicolon_separated_groups) do
local first_element = group[1]
local dialect
if first_element:find("^[a-z]+:") then
-- a dialect-specific spec
local rest
dialect, rest = first_element:match("^([a-z]+):(.*)$")
check_dialect_or_dialect_group(dialect)
group[1] = rest
end
local comma_separated_groups = put.split_alternating_runs_on_comma(group)
-- Process each value.
local outer_container = m_ca_IPA.parse_comma_separated_groups(comma_separated_groups, true, args.root,
"root")
set_parsed_respelling(dialect, outer_container)
end
else
for _, dialect_spec in ipairs(rsplit(args.root, "%s*;%s*")) do
local dialect
if dialect_spec:find("^[a-z]+:") then
-- a dialect-specific spec
local rest
dialect, rest = dialect_spec:match("^([a-z]+):(.*)$")
check_dialect_or_dialect_group(dialect)
dialect_spec = rest
end
local termobjs = {}
for _, word in ipairs(rsplit(dialect_spec, ",")) do
insert(termobjs, {words = {{term = word}}})
end
set_parsed_respelling(dialect, {
terms = termobjs,
})
end
end
-- Convert each canonicalized respelling to phonemic/phonetic IPA.
m_ca_IPA.generate_phonemic_phonetic(parsed_respellings)
-- Group the results.
local grouped_pronuns = m_ca_IPA.group_pronuns_by_dialect(parsed_respellings)
-- Format for display.
for _, grouped_pronun_spec in pairs(grouped_pronuns) do
local pronunciations = {}
local function ins(text)
insert(pronunciations, text)
end
-- Loop through each pronunciation. For each one, format the phonetic version "raw".
for j, pronun in ipairs(grouped_pronun_spec.pronuns) do
-- Add dialect tags to left accent qualifiers if first one
local as = pronun.a
if j == 1 then
if as then
as = m_table.deepCopy(as)
else
as = {}
end
for _, dialect in ipairs(grouped_pronun_spec.dialects) do
insert(as, m_ca_IPA.dialects_to_names[dialect])
end
else
ins(", ")
end
local slash_pron = "/" .. pronun.phonetic:gsub("ˈ", "") .. "/"
if as or pronun.q or pronun.qq or pronun.aa then
ins(require("Module:pron qualifier").format_qualifiers {
lang = lang,
text = slash_pron,
q = pronun.q,
a = as,
qq = pronun.qq,
aa = pronun.aa
})
else
ins(slash_pron)
end
if pronun.refs then
-- FIXME: Copied from [[Module:IPA]]. Should be in a module.
local refs = {}
if #pronun.refs > 0 then
for _, refspec in ipairs(pronun.refs) do
if type(refspec) ~= "table" then
refspec = {text = refspec}
end
local refargs
if refspec.name or refspec.group then
refargs = {name = refspec.name, group = refspec.group}
end
insert(refs, mw.getCurrentFrame():extensionTag("ref", refspec.text, refargs))
end
ins(concat(refs))
end
end
end
grouped_pronun_spec.formatted = concat(pronunciations)
end
-- Concatenate formatted results.
local formatted = {}
for _, grouped_pronun_spec in ipairs(grouped_pronuns) do
insert(formatted, grouped_pronun_spec.formatted)
end
data.post_note = "''diin sa ugat'': " .. concat(formatted, "; ")
end
end
}
-----------------------------------------------------------------------------------------
-- Numerals --
-----------------------------------------------------------------------------------------
-- Display additional inflection information for a numeral
pos_functions["numerals"] = {
params = {
[1] = true,
[2] = true,
},
func = function(args, data)
if args[1] then
insert(data.genders, "m")
parse_and_insert_inflection(data, args, 1, "pambabae")
parse_and_insert_inflection(data, args, 2, "anyong pangngalan")
else
insert(data.genders, "m")
insert(data.genders, "f")
end
end
}
-----------------------------------------------------------------------------------------
-- Phrases --
-----------------------------------------------------------------------------------------
pos_functions["phrases"] = {
params = {
["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true},
["m"] = list_param,
["f"] = list_param,
},
func = function(args, data)
validate_genders(args.g)
data.genders = args.g
parse_and_insert_inflection(data, args, "m", "panlalaki")
parse_and_insert_inflection(data, args, "f", "pambabae")
end,
}
-----------------------------------------------------------------------------------------
-- Suffix forms --
-----------------------------------------------------------------------------------------
pos_functions["suffix forms"] = {
params = {
[1] = {required = true, list = true, disallow_holes = true},
["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true},
},
func = function(args, data)
validate_genders(args.g)
data.genders = args.g
local suffix_type = {}
for _, typ in ipairs(args[1]) do
insert(suffix_type, "hulaping gumagawa ng " .. typ)
end
insert(data.inflections, {label = "anyong di-lema ng " .. m_table.serialCommaJoin(suffix_type, {conj = "o"})})
end,
}
return export
g74v0owmczg6kj5yjvillmucl030ys6
Padron:R:ca:DCVB
10
37211
178551
173747
2026-10-29T01:29:57Z
GinawaSaHapon
3989
178551
wikitext
text/x-wiki
[https://dcvb.iec.cat/results.asp?Word={{#invoke:encodings|encode|{{{1|{{pagename}}}}}|ISO 8859-1}} “{{{2|{{{1|{{pagename}}}}}}}}”] sa ''Diccionari català-valencià-balear'', Antoni Maria Alcover at Francesc de Borja Moll, 1962.<noinclude>{{documentation}}</noinclude>
bori8pyhwny35yj3y6m3f35mx4jyhgt
Module:ca-IPA
828
37840
178547
176146
2026-10-29T00:47:14Z
GinawaSaHapon
3989
178547
Scribunto
text/plain
local export = {}
local ipa_module = "Module:IPA"
local languages_module = "Module:languages"
local parse_utilities_module = "Module:parse utilities"
local string_char_module = "Module:string/char"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local m_table = require(table_module)
local deep_copy = m_table.deepCopy
local deep_equals = m_table.deepEquals
local list_to_set = m_table.listToSet
local toNFC = mw.ustring.toNFC
local u = require(string_char_module)
local lang = require(languages_module).getByCode("ca")
local function format_IPA_full(...)
format_IPA_full = require(ipa_module).format_IPA_full
return format_IPA_full(...)
end
local function pattern_escape(...)
pattern_escape = require(string_pattern_escape_module)
return pattern_escape(...)
end
local function replacement_escape(...)
replacement_escape = require(string_replacement_escape_module)
return replacement_escape(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function ufind(...)
ufind = require(string_utilities_module).find
return ufind(...)
end
local function ugcodepoint(...)
ugcodepoint = require(string_utilities_module).gcodepoint
return ugcodepoint(...)
end
local function ugsub(...)
ugsub = require(string_utilities_module).gsub
return ugsub(...)
end
local function ulower(...)
ulower = require(string_utilities_module).lower
return ulower(...)
end
local function umatch(...)
umatch = require(string_utilities_module).match
return umatch(...)
end
local function usub(...)
usub = require(string_utilities_module).sub
return usub(...)
end
--[=[
FIXME:
1. [zʒ] should reduce to [ʒ] in Central and Balearic ([[disjunt]], [[disjuntor]]). Similar for [sʃ]
([[desxifrar]]). [DONE]
2. There needs to be a way of forcing [ʃ]. (Maybe just ʃ?) [DONE]
3. Make sure manual dot for syllable break works, cf. [[best-seller]] respelled `bèst.sèlerr'. [DONE]
4. Explicit accents on a/i/u should be removed in split_syllables(). [NOT DONE; not needed]
5. Compress double schwa in Central/Balearic in e.g. [[sobreescalfament]], [[centreafricà]], [[contraatac]],
[[contraescarpa]]; seems not to operate in Valencian.
6. Compress unstressed <ie> and <oe> followed by coda consonant -> [u] in Central/Balearic in e.g. [[aeroespacial]],
[[autoescola]], [[antiespasmòdic]], but not [[autoerotisme]], [[antiemètic]]; seems not to operate in Valencian.
NOTE: It does operate in an open syllable in [[fotoelèctric]], [[fotoelectricitat]], [[macroeconomia]]; not sure why.
7. Compress unstressed <oo> followed by a coda consonant -> [u] in Central/Balearic in e.g. [[microorganisme]]. Seems
not to operate in Valencian.
8. bm -> [mm] e.g. [[subministrament]]; seems not to operate in Valencian.
9. ë (and presumably ê) doesn't work in secondary stress, always becomes /ɛ/ (e.g. in [[extrajudicial]] respelled
'ëxtrajudiciàl'; this seems to be because the handling of ë goes through mid_vowel_hint, which doesn't work for
secondary stress. [DONE]
10. Respect ʃ at beginning of word in Valencian. [DONE]
11. [ʃ] in single substitution specs should match against written x. [DONE]
12. Prefixes e.g. [[xilo-]] should not have stress by default, and written primary stresses should be converted to
secondary. [DONE]
13. Convert apostrophe near beginning to tie (‿) and make sure we take account of it later, so that words like
[[captindre's]] and phrases like [[dona d'aigua]] work correctly. [DONE]
14. Correctly handle -bl and -gl in respelling, generating [bl] and [gl]. [NOT DONE; use _bl, _gl]
15. Correctly handle [βðɣ] in respelling forcing fricatives; should not be fortitioned. [NOT DONE; not needed]
16. [βðɣ] in single substitution specs should match against b/d/g. [NOT DONE; not needed]
17. [ss] in single substitution specs should match against ss?; used to force a pronounced [s]. [DONE]
18. [dm] in single substitution specs should match against [td]m. [NOT DONE; not needed]
19. Correctly handle written -dg- after [rz]: fricatives in Valencian, stops in Central (and Balearic?). [DONE]
20. Correctly handle lenition of written -bdg-: (1) -b- not lenited in Valencian or Balearic, lenited to [β] in
Central Catalan after vowels and consonants except nasals and [rz]; (2) -g- not lenited after nasals, also not
after [rz] in Central Catalan (and maybe Balearic?), otherwise yes except utterance initial; (3) -d- not lenited
after nasals or laterals, also not after [rz] in Central Catalan (and maybe Balearic?), otherwise yes except
utterance initial. Verify against ca-IPA equivalent on cawikt and also based on {{w|Catalan phonology}} and the IEC
grammar that Vriullop linked. [DONE]
21. Finish rewriting do_dialect_specific() to operate on whole word using Lua patterns. [DONE]
22. Implement multiword handling. [DONE]
23. Make sure suffix handling works correctly. [DONE]
24. Add many more test cases and redo test harness ala the German test harness. [DONE]
25. Redo handling of mid-vowel hints so it gets done early and in one place. [DONE]
26. Think about how to solve the issue of mid-vowel hints along with secondary stress marks in substitution specs.
Maybe a single mid-vowel spec should be rewritten to be a single substitution spec and the insertion of the
mid-vowel spec should happen during resolution of substitution specs. [DONE]
27. <tm> should default to [dm] not [mm]. [DONE]
28. Fix handling of mid vowel default in -è/-ès/-esa so it doesn't affect [[tèbia]] etc. [DONE]
29. x- after hyphen should probably become tx- in Valencian, cf. [[para-xocs]]. [DONE]
30. Implement DOTOVER to indicate lack of stress in a word, e.g. in a suffix. [DONE]
31. Handle words without vowels. [DONE]
32. Finish reviewing places where we may need to check for tie symbols.
33. Handle tie indicating liaison in e.g. [[Sant Antoni de Portmany]]. [DONE]
34. Handle pronunciation of [[amb]] correctly. [DONE]
35. Handle tie indicating liaison before h- correctly, e.g. [[Sant Hipòlit]]. [DONE]
36. Lenition should happen in Valencian in [[regla]] whether respelled 'réggla', 'régla' or 'rég_la'. [DONE]
37. Syllabification should happen correctly when underscore is used in 'bíb_lia' to block doubling of <bl>. [DONE]
38. <cn> should show up as [ŋn]. [DONE]
39. Delete [t] after [s] before anything but [s] ([[best-seller]]) or [ɾ] ([[postrem]]). [DONE]
40. Delete <t/d> after <n> before consonant even in Valencian; likewise for <p/b> after <m>, <c/g> after <n>. [DONE]
41. DOTOVER in single substitution specs should work. [DONE]
42. Underline in single substitution specs should work. [DONE]
43. LINEUNDER should work to indicate secondary stress after the primary stress, including in single substitution
specs. [DONE]
]=]
export.dialects = {"bal", "cen", "val"}
export.dialects_to_names = {
bal = "Baleariko",
cen = "Gitnang Katalan",
val = "Valencia",
}
export.dialect_groups = {
east = {"bal", "cen"},
}
local written_unaccented_vowel_l = "aeiouyAEIOUY"
local written_stressed_vowel_l = "àèéêëíòóôúýÀÈÉÊËÍÒÓÔÚÝ"
local written_accented_not_stressed_vowel_l = "ïüÏÜ"
local written_accented_vowel_l = written_stressed_vowel_l .. written_accented_not_stressed_vowel_l
local ipa_vowel_l = "ɔɛə"
local written_vowel_l = written_unaccented_vowel_l .. written_accented_vowel_l
local vowel_l = written_vowel_l .. ipa_vowel_l
local V = "[" .. vowel_l .. "]"
local written_accented_to_plain_vowel = {
["à"] = "a",
["è"] = "e",
["é"] = "e",
["ê"] = "e",
["ë"] = "e",
["í"] = "i",
["ï"] = "i",
["ò"] = "o",
["ó"] = "o",
["ô"] = "o",
["ú"] = "u",
["ü"] = "u",
["ý"] = "y",
["À"] = "A",
["È"] = "E",
["É"] = "E",
["Ê"] = "E",
["Ë"] = "E",
["Í"] = "I",
["Ï"] = "I",
["Ò"] = "O",
["Ó"] = "O",
["Ô"] = "O",
["Ú"] = "U",
["Ü"] = "U",
["Ý"] = "Y",
}
local AC = u(0x0301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local CFLEX = u(0x0302) -- circumflex = ̂
local DOTOVER = u(0x0307) -- dot over = ̇
local DIA = u(0x0308) -- diaeresis = ̈
local LINEUNDER = u(0x0331) -- lineunder = ̱
local stress_l = AC .. GR
local stress_c = "[" .. stress_l .. "]"
local ipa_stress_l = "ˈˌ"
local ipa_stress_c = "[" .. ipa_stress_l .. "]"
local sylsep_l = "%-." -- hyphen included for syllabifying from spelling; FIXME: formerly included SYLDIV
local sylsep_c = "[" .. sylsep_l .. "]"
local tie_l = "‿'"
local tie_c = "[" .. tie_l .. "]"
local charsep_l = sylsep_l .. tie_l .. stress_l .. ipa_stress_l
local charsep_c = "[" .. charsep_l .. "]"
local wordsep_l = "# "
local separator_l = charsep_l .. wordsep_l
local separator_c = "[" .. separator_l .. "]"
local neg_guts_of_cons = vowel_l .. separator_l
local C = "[^" .. neg_guts_of_cons .. "]" -- consonant class including h
export.mid_vowel_hints = "éèêëóòô"
export.mid_vowel_hint_c = "[" .. export.mid_vowel_hints .. "]"
local TEMP_PAREN_R = u(0xFFF1)
local TEMP_PAREN_RR = u(0xFFF2)
-- Pseudo-consonant at the edge of prefixes ending in a vowel and suffixes beginning with a vowel; FIXME: not currently
-- used.
local PSEUDOCONS = u(0xFFF3)
-- local PREFIX_MARKER = u(0xFFF4) -- marker indicating a prefix so we can convert primary to secondary accents
local valid_onsets = list_to_set{
"b", "bl", "br",
"c", "cl", "cr",
"ç",
"d", "dj", "dr",
"f", "fl", "fr",
"g", "gl", "gr", "gu", "gü",
"h",
"i",
"j",
"k", "kl", "kr",
"l", "ll",
"m",
"n", "ny", "ñ",
"p", "pl", "pr",
"qu", "qü",
"r", "rr",
"s", "ss",
"t", "tg", "tj", "tr", "tx", "tz",
"u",
"v", "vl", "vr",
"w",
"x",
"ʃ", -- e.g. 'χruʃóf' respelling of [[Khrusxov]]
"χ", -- in case of respelling
"y",
"z",
}
local decompose_dotover = {
-- No composed i, u or U with DOTOVER.
["ȧ"] = "a" .. DOTOVER,
["ė"] = "e" .. DOTOVER,
["ȯ"] = "o" .. DOTOVER,
["ẏ"] = "y" .. DOTOVER,
["Ȧ"] = "A" .. DOTOVER,
["Ė"] = "E" .. DOTOVER,
["İ"] = "I" .. DOTOVER,
["Ȯ"] = "O" .. DOTOVER,
["Ẏ"] = "Y" .. DOTOVER,
}
local unstressed_words = list_to_set{
-- proclitic object pronouns
"em", "et", "es", "el", "la", "els", "les", "li", "ens", "us", "ho", "hi", "en",
-- enclitic object pronouns usually attach with hyphen to preceding verb but not always, cf. [[tant me fa]]
"me", "te", "se", "lo", "los", "nos", "vos", "ne",
-- contracted object pronouns and articles attached with apostrophe so no need to include
-- unstressed possessives
"mon", "ma", "mos", "mes", "ton", "ta", "tos", "tes", "son", "sa", "sos", "ses",
-- prepositions
"a", "de", "per", "amb", "ab", -- 'en' already included as proclitic object pronouns
-- prepositional contractions
"al", "als", "del", "dels", "pel", "pels",
-- articles 'el', 'la', 'els', 'les' already included as proclitic pronouns
-- personal articles
"na", -- 'en' already included above
-- indefinite articles
"un", "uns",
-- salat articles
"ets", "so", -- 'es' already included as proclitic object pronouns and 'ses', 'sa', 'sos' as possessives
-- conjunctions
"i", "o", "si", "ni", "que",
}
-- Version of ugsub() that discards all but the first return value.
local function rsub(term, foo, bar)
local retval = ugsub(term, foo, bar)
return retval
end
-- Version of ugsub() that returns a 2nd argument boolean indicating whether a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = ugsub(term, foo, bar)
return retval, nsubs > 0
end
-- Apply rsub() repeatedly until no change.
local function rsub_repeatedly(term, foo, bar)
while true do
local new_term = rsub(term, foo, bar)
if new_term == term then
return term
end
term = new_term
end
end
local function split_into_chars(text)
local chars = {}
for codepoint in ugcodepoint(text) do
table.insert(chars, u(codepoint))
end
return chars
end
local function split_on_comma(term)
if term:find(",%s") or term:find("\\") then
return require(parse_utilities_module).split_on_comma(term)
end
return split(term, ",")
end
local function concat_keys(tab)
local res = {}
for k, _ in pairs(tab) do
table.insert(res, k)
end
return table.concat(res)
end
local function handle_unstressed_words(words)
words = deep_copy(words)
-- Lowercase all words for ease in further processing.
for _, wordobj in ipairs(words) do
wordobj.term = ulower(wordobj.term)
end
-- Check if the word at index `i` in `words` is "amb" and the following word begins with a vowel.
local function is_amb_to_join(words, i)
return i < #words and words[i].term == "a" .. DOTOVER .. "mb" and ufind(words[i + 1].term, "^h?" .. V)
end
local saw_amb_to_join = true
-- Mark all unstressed words with DOTOVER, so that split_syllables() doesn't assign stress. We need to do this
-- before special handling for [[amb]], because [[amb]] may join to another unstressed word like [[el]], in the
-- process losing the identity of the two words. In the process, see if [[amb]] occurs before a following
-- vowel-initial word (which may begin with h-).
for i, wordobj in ipairs(words) do
-- Put DOTOVER after the last vowel (to handle the case of [[que]]). It doesn't actually matter where we put
-- it, because split_syllables() just looks for DOTOVER anywhere in the word.
if unstressed_words[wordobj.term] then
wordobj.term = rsub(wordobj.term, "^(.*" .. V .. ")", "%1" .. DOTOVER)
end
if is_amb_to_join(words, i) then
saw_amb_to_join = true
end
end
-- Join [[amb]] before vowel-initial word with following word.
if saw_amb_to_join then
local new_words = {}
local i = 1
while i <= #words do
if is_amb_to_join(words, i) then
table.insert(new_words, {term = words[i].term .. "‿" .. words[i + 1].term, pos = words[i + 1].pos})
i = i + 2
else
table.insert(new_words, words[i])
i = i + 1
end
end
words = new_words
end
-- Finally, rewrite some unstressed words to get the right pronunciation. Any remaining [[amb]] not before a
-- vowel-initial word is pronounced [am] even in Valencian (where [amp]/[amb] would be expected), and [[per]] always
-- has a pronounced <r>.
local unstressed_word_replacement = {
["a" .. DOTOVER .. "mb"] = "a" .. DOTOVER .. "m",
["pe" .. DOTOVER .. "r"] = "pe" .. DOTOVER .. "rr",
}
for _, wordobj in ipairs(words) do
wordobj.term = unstressed_word_replacement[wordobj.term] or wordobj.term
end
return words
end
local function fix_prefixes(word)
-- Voiced s in prefix roots -fons-, -dins-, -trans-
word = rsub(word, "^enfons([aàeèéiíoòóuú])", "enfonz%1")
word = rsub(word, "^endins([aàeèéiíoòóuú])", "endinz%1")
word = rsub(word, "tr([aà])ns([aàeèéiíoòóuúbdghlmv])", "tr%1nz%2")
-- in + ex > ineks/inegz
word = rsub(word, "^inex", "in.ex")
return word
end
local function restore_diaereses(word)
-- Some structural forms do not have diaeresis per diacritic savings, let's restore it to identify hiatus
word = rsub(word, "([iu])um(s?)$", "%1üm%2") -- Latinisms (-ius is ambiguous but rare)
word = rsub(word, "([aeiou])isme(s?)$", "%1ísme%2") -- suffix -isme
word = rsub(word, "([aeiou])ist([ae]s?)$", "%1íst%2") -- suffix -ista
word = rsub(word, "([aeou])ir$", "%1ír") -- verbs -ir
word = rsub(word, "([aeou])int$", "%1ínt") -- present participle
word = rsub(word, "([aeo])ir([éà])$", "%1ïr%2") -- future
word = rsub(word, "([^gq]u)ir([éà])$", "%1ïr%2")
word = rsub(word, "([aeo])iràs$", "%1ïràs")
word = rsub(word, "([^gq]u)iràs$", "%1ïràs")
word = rsub(word, "([aeo])ir(e[mu])$", "%1ïr%2")
word = rsub(word, "([^gq]u)ir(e[mu])$", "%1ïr%2")
word = rsub(word, "([aeo])iran$", "%1ïran")
word = rsub(word, "([^gq]u)iran$", "%1ïran")
word = rsub(word, "([aeo])iria$", "%1ïria") -- conditional
word = rsub(word, "([^gq]u)iria$", "%1ïria")
word = rsub(word, "([aeo])ir(ie[sn])$", "%1ïr%2")
word = rsub(word, "([^gq]u)ir(ie[sn])$", "%1ïr%2")
return word
end
local function fix_y(word)
-- y > vowel i else consonant /j/, except ny
word = rsub(word, "ny", "ñ")
word = rsub(word, "y([^aeiouàèéêëíòóôúïü])", "i%1") -- vowel if not next to another vowel
word = rsub(word, "([^aeiouàèéêëíòóôúïü·%-%.])y", "%1i") -- excluding also syllables separators
return word
end
local function mid_vowel_fixes(word)
local function track_mid_vowel(vowel, cont)
require("Module:debug/track"){"ca-IPA/" .. vowel, "ca-IPA/" .. vowel .. "/" .. cont}
return true
end
local changed
-- final -el (not -ell) usually è but not too many cases
word, changed = rsubb(word, "e(nts?)$", "é%1")
if changed then
track_mid_vowel("e", "nt-nts")
end
word, changed = rsubb(word, "e(rs?)$", "é%1")
if changed then
track_mid_vowel("e", "r-rs")
end
word, changed = rsubb(word, "o(rs?)$", "ó%1")
if changed then
track_mid_vowel("o", "r-rs")
end
word, changed = rsubb(word, "è(s?)$", "ê%1")
if changed then
track_mid_vowel("è", "s-blank")
end
word, changed = rsubb(word, "e(s[oe]s)$", "ê%1")
if changed then
track_mid_vowel("e", "sos-sa-ses")
end
word, changed = rsubb(word, "e(sa)$", "ê%1")
if changed then
track_mid_vowel("e", "sos-sa-ses")
end
return word
end
local function word_fixes(word, dialect)
word = rsub(word, "%(rr%)", TEMP_PAREN_RR)
word = rsub(word, "%(r%)", TEMP_PAREN_R)
word = rsub(word, "%-([rs]?)", "-%1%1")
if dialect == "val" then
word = rsub(word, "%-x", "-tx")
end
word = rsub(word, "rç$", "rrs") -- silent r only in plurals -rs
word = fix_prefixes(word) -- internal pause after a prefix
word = restore_diaereses(word) -- no diaeresis saving
word = fix_y(word) -- ny > ñ else y > i vowel or consonant
word = mid_vowel_fixes(word)
-- all words in pn- (e.g. [[pneumotòrax]] and mn- (e.g. [[mnemònic]]) have silent p/m in both Central and Valencian
word = rsub(word, "^[pm]n", "n")
-- Respell ch + vowel as tx, before we remove other h's after consonants.
word = rsub(word, "ch(" .. V ..")", "tx%1")
-- Delete h after a consonant. This must happen here, before split_syllables(). We don't delete h after a vowel
-- yet because it indicates a hiatus.
word = rsub(word, "(" .. C .. ")h", "%1")
return word
end
local function split_vowels(vowels, saw_dotover, saw_lineunder)
local syllables = {{onset = "", vowel = usub(vowels, 1, 1), coda = "", separator = "", has_dotover = saw_dotover,
has_lineunder = saw_lineunder}}
vowels = usub(vowels, 2)
while vowels ~= "" do
local syll = {onset = "", vowel = "", coda = ""}
syll.onset, syll.vowel, vowels = umatch(vowels, "^([iu]?)(.)(.-)$")
table.insert(syllables, syll)
end
local count = #syllables
if count >= 2 and (syllables[count].vowel == "i" or syllables[count].vowel == "u") then
syllables[count - 1].coda = syllables[count].vowel
syllables[count] = nil
end
return syllables
end
-- Split the word into syllables. Return a list of syllable objects, each of which contains fields `onset`, `vowel`,
-- `coda`, `separator` (a user-specified syllable divider that goes before the syllable; one of '·', '-' or '.') and
-- `stressed` (a boolean indicating that the syllable is stressed). In addition, the list has fields `stress` (the
-- index of the syllable with primary stress) and `is_prefix` (true if the word is a prefix, i.e. it ends in '-').
-- Normally, prefixes are treated as unstressed if a stressed syllable isn't explicitly marked, but this can be
-- overridden with `stress_prefixes`, which causes the automatic stress-assignment algorithm to run for these terms.
local function split_syllables(word, stress_prefixes, may_be_uppercase)
local syllables = {}
local saw_dotover = false
local remainder = word
local is_prefix = false
if remainder:find("%-$") then -- prefix
is_prefix = true
remainder = remainder:gsub("%-$", "")
end
local is_suffix = false
if remainder:find("^%-") then -- suffix
is_suffix = true
remainder = remainder:gsub("^%-", "")
end
while remainder ~= "" do
local consonants, vowels
-- FIXME: Using C and V below instead of the existing patterns slows things down TREMENDOUSLY.
-- Not sure why.
local vowel_list = may_be_uppercase and "aeiouàèéêëíòóôúïüAEIOUÀÈÉÊËÍÒÓÔÚÏÜ" .. DOTOVER .. LINEUNDER or
"aeiouàèéêëíòóôúïü" .. DOTOVER .. LINEUNDER
consonants, remainder = umatch(remainder, "^([^" .. vowel_list .. "]*)(.-)$")
vowels, remainder = umatch(remainder, "^([" .. vowel_list .. "]*)(.-)$")
local this_saw_dotover = not not ufind(vowels, DOTOVER)
if this_saw_dotover then
saw_dotover = true
vowels = vowels:gsub(DOTOVER, "")
end
local this_saw_lineunder = not not ufind(vowels, LINEUNDER)
if this_saw_lineunder then
vowels = vowels:gsub(LINEUNDER, "")
end
if vowels == "" then
if #syllables > 0 then
syllables[#syllables].coda = syllables[#syllables].coda .. consonants
else
-- word without vowels, e.g. foot boundary |
table.insert(syllables, {onset = consonants, vowel = "", coda = "", separator = ""})
end
else
local onset = consonants
local first_vowel = usub(vowels, 1, 1)
if (ufind(onset, "[gqGQ]$") and (first_vowel == "ü" or (first_vowel == "u" and vowels ~= "u")))
or ((onset == "" or onset == "h" or onset == "H") and #syllables == 0 and
(first_vowel == "i" or first_vowel == "I") and (vowels ~= "i" and vowels ~= "I"))
then
onset = onset .. usub(vowels, 1, 1)
vowels = usub(vowels, 2)
end
local vsyllables = split_vowels(vowels, this_saw_dotover, this_saw_lineunder)
vsyllables[1].onset = onset .. vsyllables[1].onset
for _, s in ipairs(vsyllables) do
table.insert(syllables, s)
end
end
end
-- Shift over consonants from the onset to the preceding coda, until the syllable onset is valid
for i = 2, #syllables do
local current = syllables[i]
local previous = syllables[i-1]
while not (current.onset == "" or valid_onsets[rsub(rsub(current.onset, tie_c .. "[hH]?$", ""), "_", "")]) do
local letter = usub(current.onset, 1, 1)
current.onset = usub(current.onset, 2)
if ufind(letter, "[·%-%.]") then -- syllable separators
current.separator = letter
break
else
previous.coda = previous.coda .. letter
if ufind(letter, tie_c) then
break
end
end
end
end
-- Detect stress
for i, syll in ipairs(syllables) do
if ufind(syll.vowel, "^[" .. written_stressed_vowel_l .. "]$") then
syll.stressed = true
-- primary stress: the last one stressed without LINEUNDER
if not syll.has_lineunder then
syllables.stress = i
end
end
end
-- Assign default stress
if not syllables.stress and not saw_dotover and (stress_prefixes or not is_prefix) then
local count = #syllables
if count == 1 then
if syllables[1].vowel ~= "" then -- vowel-less words don't get stress
syllables.stress = 1
end
else
local final = syllables[count]
-- Take account of tie symbols (apostrophes and ‿).
if ufind(final.coda, "^[s" .. tie_l .. "]*$") or (ufind(final.coda, "^" .. tie_c .. "*n" .. tie_c .. "*$") and (
final.vowel == "e" or final.vowel == "i" or final.vowel == "ï")) then
syllables.stress = count - 1
else
syllables.stress = count
end
end
if syllables.stress then
syllables[syllables.stress].stressed = true
end
end
syllables.is_prefix = is_prefix
syllables.is_suffix = is_suffix
return syllables
end
local function reconstitute_word_from_syllables(syllables)
local parts = {}
local function ins(txt)
table.insert(parts, txt)
end
if syllables.is_suffix then
ins("-")
end
for _, syl in ipairs(syllables) do
ins(syl.separator)
ins(syl.onset)
ins(syl.vowel)
if syl.has_dotover then
ins(DOTOVER)
end
if syl.has_lineunder then
ins(LINEUNDER)
end
ins(syl.coda)
end
if syllables.is_prefix then
ins("-")
end
return table.concat(parts)
end
local function decompose_respelling(text)
local dotover_keys = concat_keys(decompose_dotover)
return rsub(text, "[" .. dotover_keys .. "]", decompose_dotover)
end
local function canon_respelling(text)
local function canon_spaces(text)
text = rsub(text, "%s+", " ")
text = rsub(text, "^ ", "")
text = rsub(text, " $", "")
return text
end
text = canon_spaces(text)
-- eliminate upside down punctuation
text = rsub(text, "[¡¿]", "")
-- eliminate utterance-final punctuation
text = rsub(text, "[!?.]$", "")
-- eliminate double and triple quotes
text = rsub(text, "''+", "")
-- Convert commas and em/en dashes to IPA foot boundaries; require a space after commas and en dashes (for the
-- latter, in particular, to avoid treating the en dash in 'Bose–Einstein condensate' as a foot boundary.
text = rsub(text, " *[,–] ", " | ")
text = rsub(text, " *[—] *", " | ")
-- ... in phrases like [[com es diu...en català]] and [[necessito ...]] become foot boundaries
text = rsub(text, " *%.%.%. *", " | ")
-- remaining commas and en dashes become spaces
text = rsub(text, "[,–]", " ")
-- may need to eliminate extraneous spaces again, e.g. if there was a space before or after an eliminated
-- punctuation mark
text = canon_spaces(text)
-- question mark or exclamation point in the middle of a sentence -> IPA foot boundary
text = rsub(text, "([^ ]) *[!?] *([^ ])", "%1 | %2")
return text
end
local IPA_vowels_central = {
["ê"] = "ɛ", ["ë"] = "ɛ", ["ô"] = "ɔ",
}
local IPA_vowels_balearic = {
["ê"] = "ə", ["ë"] = "ɛ", ["ô"] = "ɔ",
}
local IPA_vowels_valencian = {
["ê"] = "e", ["ë"] = "e", ["ô"] = "o",
}
local IPA_vowels = {
["à"] = "a",
["è"] = "ɛ", ["ê"] = "ɛ", ["ë"] = "ɛ", ["é"] = "e",
["í"] = "i", ["ï"] = "i",
["ò"] = "ɔ", ["ô"] = "ɔ", ["ó"] = "o",
["ú"] = "u", ["ü"] = "u",
}
local function replace_context_free(cons)
cons = rsub(cons, "ŀ", "l")
cons = rsub(cons, "r", "ɾ")
cons = rsub(cons, "ɾɾ", "r")
cons = rsub(cons, "ss", "s")
cons = rsub(cons, "ll", "ʎ")
cons = rsub(cons, "ñ", "ɲ") -- hint ny > ñ
-- NOTE: We use single-character affricate symbols during processing for ease in handling, and convert them
-- to tied multi-character affricates at the end of join_syllables().
cons = rsub(cons, "[dt]j", "ʤ")
cons = rsub(cons, "tx", "ʧ")
cons = rsub(cons, "[dt]z", "ʣ")
cons = rsub(cons, "ç", "s")
cons = rsub(cons, "[cq]", "k")
cons = rsub(cons, "h", "")
cons = rsub(cons, "j", "ʒ")
-- Don't replace x -> ʃ yet so we can distinguish x from manually specified ʃ.
cons = rsub(cons, "i", "j") -- must be after j > ʒ
cons = rsub(cons, "y", "j") -- must be after j > ʒ and fix_y
cons = rsub(cons, "[uü]", "w")
cons = rsub(cons, "'", "‿")
return cons
end
-- Do context-sensitive phonological changes. Formerly this was all done syllable-by-syllable but that made the code
-- tricky (since it often had to look at adjacent syllables) and full of subtle bugs. Now we first concatenate the
-- syllables back to words and the words to the combined text and work on the text as a whole. FIXME: We should move
-- more of the work done in preprocess_word(), e.g. most of replace_context_free(), here.
local function postprocess_general(text, dialect)
local function verify(cond, msg)
if not cond then
error(("Internal error: %s; processed respelling at this point is '%s'"):format(msg, text))
end
return true
end
local voiced = list_to_set{"b", "d", "g", "m", "n", "ɲ", "l", "ʎ", "r", "ɾ", "v", "z", "ʒ", "ʣ", "ʤ"}
local voiceless = list_to_set{"p", "t", "k", "f", "s", "ʃ", "ʦ", "ʧ"}
local voicing = {["p"] = "b", ["t"] = "d", ["k"] = "g", ["f"] = "v", ["s"] = "z", ["ʃ"] = "ʒ", ["ʦ"] = "ʤ",
["ʧ"] = "ʤ"}
local devoicing = {}
for k, v in pairs(voicing) do
devoicing[v] = k
end
------------------ Handle <x>
-- Handle ex- + vowel > -egz-. We handle -x- on either side of the syllable boundary. Note that this also handles
-- inex- + vowel because in fix_prefixes we respell inex- as in.ex-, which ends up at this stage as in.e.xV.
text = rsub_repeatedly(text, "([.#][eɛ]" .. stress_c .. "*)(" .. charsep_c .. "*)x(" .. charsep_c .. "*" .. V ..
")", function(e, pre, post)
-- Preserve other character separators (especially the tie character ‿).
pre = pre:gsub("%.", "")
post = post:gsub("%.", "")
return e .. pre .. "g.z" .. post
end)
-- -x- at the beginning of a coda becomes [ks], e.g. [[annex]], [[apèndix]], [[extracció]]; but not elsewhere in
-- the coda, e.g. in [[romanx]], [[ponx]]; words with [ks] in -nx such as [[esfinx]], [[linx]], [[manx]] need
-- respelling with [ks]; words ending in vowel + x like [[ídix]] need respelling with [ʃ]
text = rsub(text, "(" .. V .. stress_c .. "*)x", "%1ks")
if dialect == "val" then
-- Word-initial <x> as well as <x> after a consonant other than /j/ (including in the coda, e.g. [[ponx]])
-- becomes [t͡ʃ].
text = rsub(text, "#x", "#ʧ")
text = rsub(text, "([^" .. vowel_l .. separator_l .. "j]" .. charsep_c .. "*)x", "%1ʧ")
end
-- Other x becomes [ʃ]
text = rsub(text, "x", "ʃ")
-- Doubled ss -> s e.g. in exs-, exc(e/i)-, sc(e/i)-; FIXME: should this apply across word boundaries?
text = rsub(text, "s(" .. charsep_c .. "*)s", "%1s")
------------------ Coda consonant losses
-- In Central Catalan, coda losses happen everywhere, but otherwise they don't happen when
-- absolutely word-finally before a vowel or end of utterance (e.g. [[blanc]] has /k/ in Balearic and
-- Valencian but not [[blancs]]). Must precede consonant assimilations.
local boundary = dialect == "cen" and "(.)" or "([^#])"
text = rsub(text, "m[pb]" .. boundary, "m%1")
text = rsub(text, "([ln])[td]" .. boundary, "%1%2")
text = rsub(text, "[nŋ][kg]" .. boundary, "ŋ%1")
if dialect == "val" or dialect == "bal" then
local before_cons = "(" .. separator_c .. "*" .. C .. ")"
text = rsub(text, "m[pb]" .. before_cons, "m%1")
text = rsub(text, "([ln])[td]" .. before_cons, "%1%2")
text = rsub(text, "[nŋ][kg]" .. before_cons, "ŋ%1")
end
-- Delete /t/ between /s/ and any consonant other than /s/ or /ɾ/. Must precede voicing assimilation and
-- t + lateral/nasal assimilation.
text = rsub(text, "st(" .. sylsep_c .. "*[^" .. neg_guts_of_cons .. "sɾ])", "s%1")
------------------ Consonant assimilations
if dialect == "cen" then
-- v > b in onsets (not in codas, e.g. [[ovni]] [ɔ́vni] and [[hafni]] [ávni]). This needs to precede
-- assimilation of nb -> mb.
text = rsub(text, "v(" .. C .. "*" .. V ..")", "b%1")
end
-- t + lateral assimilation -> geminate across syllable boundary. We don't any more do t + nasal assimiation
-- because there are too many exceptions, e.g. [[aritmètic]], [[atmosfèric]], [[ètnia]]. Instead, we require that
-- cases where it does happen use respelling to effect this. FIXME: this doesn't always happen in -tl- either,
-- e.g. [[atlàntic]] has [əllántik] in GDLC but [adlántik] in DNV.
--
-- FIXME: Clean this up, maybe move below voicing assimilation, investigate whether it operates across words,
-- move stuff below that special-cases tll in Valencian here.
text = rsub(text, "t(" .. sylsep_c .. ")([lʎ])", "%2%1%2")
-- n + labial > labialized assimilation
text = rsub(text, "n(" .. separator_c .. "*[mbp])", "m%1")
text = rsub(text, "n(" .. separator_c .. "*[fv])", "ɱ%1")
-- n + velar > velarized assimilation
text = rsub(text, "n(" .. separator_c .. "*[kg])", "ŋ%1")
-- l/n + palatal > palatalized assimilation
text = rsub(text, "([ln])(" .. separator_c .. "*[ʎɲʃʒʧʤ])", function(ln, palatal)
ln = ({["l"] = "ʎ", ["n"] = "ɲ"})[ln]
return ln .. palatal
end)
-- ɲs > ɲʃ; FIXME: not sure the purpose of this; it doesn't apply in [[menys]] or derived terms like [[menyspreu]]
-- NOTE: Per [https://giec.iec.cat/textgramatica/codi/4.4], it does apply in these scenarios but the result is
-- somewhere between [s] and [ʃ], which is why it isn't shown in GDLC.
-- text = rsub(text, "ɲs", "%1ʃ")
------------------ Handle <r>
-- In replace_context_free(), we converted single r to ɾ and double rr to r.
if dialect == "cen" then
text = rsub(text, TEMP_PAREN_R, "")
text = rsub(text, TEMP_PAREN_RR, "r")
elseif dialect == "bal" then
text = rsub(text, TEMP_PAREN_R, "")
text = rsub(text, TEMP_PAREN_RR, "")
else
verify(dialect == "val", ("Unrecognized dialect '%s'"):format(dialect))
text = rsub(text, TEMP_PAREN_R, "ɾ")
text = rsub(text, TEMP_PAREN_RR, "ɾ")
end
if dialect ~= "val" then
-- Coda /ɾ/ -> /r/
-- FIXME: This is inherited from the older code. Correct?
text = rsub(text, "(" .. V .. stress_c .. "*" .. C .. "*)ɾ", "%1r")
end
-- ɾ -> r word-initially or after [lns]; needs to precede voicing assimilation as <s> will be voiced to [z] before
-- /ɾ/.
text = rsub(text, "([#lns]" .. sylsep_c .. "*)ɾ", "%1r")
------------------ Voicing assimilation
-- Voicing or devoicing; we want to proceed from right to left, and due to the limitations of patterns (in
-- particular, the lack of support for alternations), it's difficult to do this cleanly using Lua patterns, so we
-- do it character by character.
local chars = split_into_chars(text)
-- We need to look two characters ahead in some cases, so start two characters from the end. This is safe because
-- the overall respelling ends in "##". (Similarly, as an optimization, don't check the first two characters, which
-- are always "##".)
for i = #chars - 2, 3, -1 do
-- We are looking for two consonants next to each other, possibly separated by a syllable or word divider.
-- We also handle a consonant followed by a syllable divider then a vowel, and a consonant word-finally.
-- Note that only coda consonants can change voicing, so we need to check to make sure we're in the coda.
local first = chars[i]
-- If `second` is nil, no assimilation occurs. Otherwise, `second` should be a consonant or empty string (which
-- represents a syllable or word boundary followed by a vowel or end of string), and we assimilate to that
-- consonant (empty string forces devoicing).
local second
-- If set to true, we're processing a consonant directly before a word boundary followed by a word beginning
-- with a vowel. In this context, voiceless sibilants voice. Note that we handle voicing of <s> word-internally
-- separately, in preprocess_word() [FIXME: maybe move much of the processing in preprocess_word() into this
-- function].
local word_boundary_before_vowel
if not ufind(first, C) then
-- leave `second` at nil; no assimilation
elseif chars[i + 1] == "#" then -- word boundary
if chars[i + 2] == " " then
-- chars[i + 3] should always be "#"
verify(chars[i + 3] == "#", "Word boundary followed by space but not #")
if ufind(chars[i + 4], C) then
second = chars[i + 4]
else
second = ""
word_boundary_before_vowel = true
end
else
second = ""
end
elseif ufind(chars[i + 1], sylsep_c) then -- syllable boundary
if ufind(chars[i + 2], C) then
second = chars[i + 2]
else
second = ""
end
elseif ufind(chars[i + 1], C) then
second = chars[i + 1]
else
-- followed by a vowel not across a syllable or word boundary; leave `second` as nil, no assimilation
end
if second then
-- Make sure we're in the coda. We have to look backwards until we find a vowel or syllable/word boundary.
local in_coda = false
local j = i - 1
while true do
verify(j > 0, "Missing word boundary at beginning of overall respelling")
if ufind(chars[j], "[" .. sylsep_l .. wordsep_l .. "]") then
break
elseif ufind(chars[j], V) then
in_coda = true
break
end
j = j - 1
end
if in_coda then
if word_boundary_before_vowel and ufind(first, "[zʒʣʤ]") then
-- leave alone
elseif voiced[second] and voicing[first] or word_boundary_before_vowel and ufind(first, "[sʃʦʧ]") then
chars[i] = voicing[first]
elseif (voiceless[second] or second == "") and devoicing[first] then
chars[i] = devoicing[first]
end
end
end
end
text = table.concat(chars)
-- gn -> ŋn e.g. [[regnar]] (including word-initial gn- e.g. [[gnòmic]], [[gneis]])
-- FIXME: This should be moved below voicing assimilation, and we need to investigate if it operates across words
-- (here I'm guessing yes).
if dialect ~= "cen" then
text = rsub(text, "#gn", "#n")
end
text = rsub(text, "g(" .. separator_c .. "*n)", "ŋ%1")
-- gʒ > d͡ʒ
-- FIXME: We need to investigate if it operates across words
text = rsub(text, "g(" .. sylsep_c .. "*)ʒ", "%1ʤ")
-- sʃ -> ʃ ([[desxifrar]]), zʒ -> ʒ ([[disjuntor]])
if dialect ~= "val" then
text = rsub(text, "s(" .. separator_c .. "*ʃ)", "%1")
text = rsub(text, "z(" .. separator_c .. "*ʒ)", "%1")
end
------------------ Gemination of <bl>, <gl>
if dialect ~= "val" then
-- bl -> bbl, gl -> ggl after the stress when following a vowel; to avoid this, use <b_l> or <g_l>.
-- This must follow v > b above. To force a hard ungeminated [b] or [g], use <_b> or <_g>.
text = rsub(text, "(" .. stress_c .. ")(" .. sylsep_c .. ")([bg])l", "%1%3%2%3l")
else -- Valencian; undo manually written 'bbl', 'ggl' in words like [[poblar]], [[reglament]]
text = rsub(text, "([bg])(" .. sylsep_c .. ")%1l", "%2%1l")
end
------------------ Lenition of voiced stops
-- In Central Catalan, b/d/g become fricatives (actually approximants, like in Spanish) in the onset following a
-- vowel and (except for <d>) after <l> and <ll> (cf. GDLC [[cabellblanc]] [kəβɛ̀ʎβláŋ]). This also happens across
-- word boundaries but doesn't happen after stops, nor in Central Catalan after [r], [ɾ] or [z] (and hence probably
-- not after [ʒ] either, although I can't find any examples in GDLC).
--
-- In Valencian, <b> doesn't lenite (at least formally?), but <d> and <g> do lenite after [r], [ɾ] or [z].
--
-- Balearic is like Valencian in not leniting <b>, and probably like Central Catalan otherwise.
local lenite_bdg = {["b"] = "β", ["d"] = "ð", ["g"] = "ɣ"}
if dialect == "cen" then
text = rsub(text, "([" .. vowel_l .. "jwv]" .. separator_c .. "*[.#]" .. separator_c .. "*)([bdg])",
function(before, bdg) return before .. lenite_bdg[bdg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)([bg])",
function(before, bg) return before .. lenite_bdg[bg] end)
elseif dialect == "val" then
text = rsub(text, "([" .. vowel_l .. "jwvrɾzʣ]" .. separator_c .. "*[.#]" .. separator_c .. "*)([dg])",
function(before, dg) return before .. lenite_bdg[dg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)(g)",
function(before, g) return before .. lenite_bdg[g] end)
else
verify(dialect == "bal", ("Unrecognized dialect '%s'"):format(dialect))
text = rsub(text, "([" .. vowel_l .. "jwv]" .. separator_c .. "*[.#]" .. separator_c .. "*)([dg])",
function(before, dg) return before .. lenite_bdg[dg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)(g)",
function(before, g) return before .. lenite_bdg[g] end)
end
------------------ Vowel reduction
-- Reduction of unstressed a,e in Central and Balearic (Eastern Catalan).
if dialect ~= "val" then
-- The following rules seem to apply, based on the old code:
-- (1) Stressed a and e are never reduced.
-- (2) Unstressed e directly following ə is not reduced.
-- (3) Unstressed e directly before written <a> or before /ɔ/ is not reduced.
-- (4) Written <ee> when both vowels precede the primary stress is reduced to [əə]. (This rule preempts #2.)
-- (5) Written <ee> when both vowels follow the primary stress isn't reduced at all.
-- Rule #2 in particular seems to require that we proceed left to right, which is how the old code was
-- implemented.
-- FIXME: These rules seem overly complex and may produce incorrect results in some circumstances.
local words = split(text, " ")
for j, word in ipairs(words) do
local chars = split_into_chars(word)
-- See above where voicing assimilation is handled. The overall respelling begins and ends in #, which we
-- can ignore. We need to look ahead three chars in some circumstances, but in all those circumstances we
-- shoudn't run off the end (and have assertions to check this).
local seen_primary_stress = false
for i = 2, #chars - 1 do
local this = chars[i]
if chars[i] == AC then
seen_primary_stress = true
end
if (this ~= "a" and this ~= "e") or ufind(chars[i + 1], stress_c) then
-- Not a/e, or a stressed vowel; continue
else
local reduction = true
local prev, prev_stress, nxt, nxt_stress
if not ufind(chars[i - 1], sylsep_c) then
prev = ""
else
prev = chars[i - 2] -- this should be non-nil as chars[i - 1] is a syllable separator (not #)
verify(prev, "Missing # at word boundary")
prev_stress = ""
if ufind(prev, stress_c) then
prev_stress = prev
prev = chars[i - 3]
-- As above; chars[i - 2] is a stress indicator (not #).
verify(prev, "Missing # at word boundary")
end
end
if not ufind(chars[i + 1], sylsep_c) then
nxt = ""
-- leave nxt at nil
else
nxt = chars[i + 2]
nxt_stress = chars[i + 3]
-- chars[i + 1] is a syllable separator, so chars[i + 2] should not be a word boundary, so
-- chars[i + 3] should exist.
verify(nxt and nxt_stress, "Syllable separator at word boundary or missing # at word boundary")
end
if this == "e" and ufind(prev, "ə") then
reduction = false
elseif this == "e" and ufind(nxt, "[aɔ]") then
reduction = false
elseif this == "e" and nxt == "e" and not ufind(nxt_stress, AC) then
-- FIXME: Check specifically for AC duplicates previous logic but is probably wrong or unnecessary.
if not seen_primary_stress then
chars[i + 2] = "ə"
else
reduction = false
end
end
if reduction then
chars[i] = "ə"
end
end
end
words[j] = table.concat(chars)
end
text = table.concat(words, " ")
end
if dialect == "cen" then
-- Reduction of unstressed o (not before w)
text = rsub(text, "o([^" .. stress_l .. "w])", "u%1")
elseif dialect == "bal" then
-- Reduction of unstressed o per vowel harmony: unstressed /o/ -> /u/ directly before stressed /i/ or /u/;
-- as a Lua pattern, o can be followed only by consonants and/or syllable separators (no vowels, stress marks
-- or word separators).
text = rsub(text, "o([^" .. vowel_l .. stress_l .. wordsep_l .. "]*[iu]" .. stress_c .. ")", "u%1")
end
-- Final losses.
text = rsub(text, "j(ʧs?#)", "%1") -- boigs /bɔt͡ʃ/
text = rsub(text, "([ʃʧs])s#", "%1#") -- homophone plurals -xs, -igs, -çs
if dialect ~= "val" then
-- Remove j before palatal obstruents
text = rsub(text, "j(" .. sylsep_c .. "*[ʃʒʧʤ])", "%1")
else -- Valencian
-- Fortition of palatal fricatives
text = rsub(text, "ʒ", "ʤ")
text = rsub(text, "(i" .. stress_c .. "*" .. sylsep_c .. ")ʣ", "%1z")
end
if dialect ~= "cen" then
-- No palatal gemination ʎʎ > ll or ʎ, in Valencian and Balearic.
-- FIXME: These conditions seem to be targeting specific words and should probably be fixed using respelling
-- instead.
text = rsub(text, "([bpw]a" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "([mv]e" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(ti" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(m[oɔ]" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(u" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "ʎ(" .. sylsep_c .. "*ʎ)", "%1")
end
---------- Convert pseudo-symbols to real ones.
-- Convert g to IPA ɡ.
text = rsub(text, "g", "ɡ")
-- Convert pseudo-afficate symbols to full affricates.
local full_affricates = { ["ʦ"] = "t͡s", ["ʣ"] = "d͡z", ["ʧ"] = "t͡ʃ", ["ʤ"] = "d͡ʒ" }
text = rsub(text, "([ʦʣʧʤ])", full_affricates)
---------- Generate IPA stress marks.
-- Convert acute and grave to IPA stress marks.
text = rsub(text, AC, "ˈ")
text = rsub(text, GR, "ˌ")
-- Move IPA stress marks to the beginning of the syllable.
text = rsub_repeatedly(text, "([#.])([^#.]*)(" .. ipa_stress_c .. ")", "%1%3%2")
-- Suppress syllable divider before IPA stress indicator.
text = rsub(text, "%.(#?" .. ipa_stress_c .. ")", "%1")
-- Make all primary stresses but the last one in a given word be secondary. May be fed by the first rule above.
-- FIXME: Currently this is handled earlier, but we might want to move it here, as is done in [[Module:pt-pronunc]].
-- text = rsub_repeatedly(text, "ˈ([^ ]+)ˈ", "ˌ%1ˈ")
-- Make primary stresses in prefixes become secondary. (FIXME: Handled earlier now.)
-- text = rsub_repeatedly(text, "ˈ([^#]*#" .. PREFIX_MARKER .. ")", "ˌ%1")
-- Remove # symbols at word/text boundaries, as well as _ (which forces separate interpretation), pseudo-consonant
-- markers (at edges of some prefixes/suffixes), and prefix markers, and recompose.
text = rsub(text, "[#_" .. PSEUDOCONS .. "]", "")
text = toNFC(text)
return text
end
local function preprocess_word(syllables, suffix_syllables, dialect, pos, orig_word)
-- Stressed vowel is ambiguous
if syllables.stress then
local stressed_vowel = syllables[syllables.stress].vowel
if ufind(stressed_vowel, "[eo]") then
local marks = {["e"] = {AC, GR, CFLEX, DIA}, ["o"] = {AC, GR, CFLEX}}
local marked_vowels = {}
for _, mark in ipairs(marks[stressed_vowel]) do
table.insert(marked_vowels, stressed_vowel .. mark)
end
error(("In respelling '%s', the stressed vowel '%s' is ambiguous. Please mark it with an acute, " ..
"grave, or combined accent: %s."):format(orig_word, stressed_vowel,
mw.text.listToText(marked_vowels, nil, " or ")))
end
end
-- Final -r is ambiguous in many cases.
local final = syllables[#syllables]
-- Stressed final r after a or i in non-monosyllables is treated as (r), i.e. verbal infinitives are assumed (NOTE:
-- not always the case, e.g. there are many adjectives and nouns in -ar that should be marked as '(rr)', and
-- several loanword nouns in -ir that should be marked as 'rr'). Likewise for stressed final r or rs after é in
-- non-monosyllables (which are usually adjectives or nouns with the -er ending, but may be verbal infinitives,
-- which should be marked as 'ê(r)'). That is, it disappears other than in Valencian. All other final r and final
-- rs are considered ambiguous and need to be rewritten using rr, (rr) or (r).
if #syllables > 1 and final.stressed then
if final.coda == "r" and ufind(final.vowel, "[aàiíé]") or final.coda == "rs" and final.vowel == "é" or
final.vowel == "ó" and ufind(final.coda, "^rs?$") and ufind(final.onset, "[stdç]") then
final.coda = TEMP_PAREN_R
end
end
if ufind(final.coda, "^rs?$") or ufind(final.coda, "[^r]rs?$") then
error(("In respelling '%s', final -r by itself or in -rs is ambiguous except in the verbal endings -ar or " ..
"-ir, in the nominal or adjectival endings -er(s) and -[dtsç]or(s). In all other cases it needs to be " ..
"rewritten using one of 'rr' (pronounced everywhere), '(rr)' (pronounced everywhere but Balearic) or " ..
"'(r)' (pronounced only in Valencian). Note that adjectives in -ar usually need rewriting using '(rr)'; " ..
"nouns in -ar referring to places should be rewritten using '(r)'; and loanword nouns in -ir usually " ..
"need rewriting using 'rr'."):format(orig_word))
end
local syllables_IPA = {stress = syllables.stress, is_prefix = syllables.is_prefix, is_suffix = syllables.is_suffix}
for key, val in ipairs(syllables) do
syllables_IPA[key] = {onset = val.onset, vowel = val.vowel, coda = val.coda, stressed = val.stressed}
end
-- Replace letters with IPA equivalents
for i, syll in ipairs(syllables_IPA) do
-- Voicing of s
if syll.onset == "s" and i > 1 and ufind(syllables[i - 1].coda, "^[iu]?$") then
syll.onset = "z"
end
if ufind(syll.vowel, "^[eèéêëií]$") then
syll.onset = rsub(syll.onset, "tg$", "ʤ")
syll.onset = rsub(syll.onset, "[cg]$", {["c"] = "s", ["g"] = "ʒ"})
syll.onset = rsub(syll.onset, "[qg]u$", {["qu"] = "k", ["gu"] = "g"})
end
syll.coda = rsub(syll.coda, "igs?$", "iʤ")
syll.onset = replace_context_free(syll.onset)
syll.coda = replace_context_free(syll.coda)
syll.vowel = rsub(syll.vowel, ".",
dialect == "cen" and IPA_vowels_central or
dialect == "bal" and IPA_vowels_balearic or
IPA_vowels_valencian
)
syll.vowel = rsub(syll.vowel, ".", IPA_vowels)
end
for _, suffix_syl in ipairs(suffix_syllables) do
table.insert(syllables_IPA, suffix_syl)
end
return syllables_IPA
end
-- Given a single substitution spec, `to`, figure out the corresponding value of `from` used in a complete
-- substitution spec. `pagename` is the name of the page, either the actual one or taken from the `pagename` param.
-- `whole_word`, if set, indicates that the match must be to a whole word (it was preceded by ~).
local function convert_single_substitution_to_original(to, pagename, whole_word)
-- Replace specially-handled characters with a class matching the character and possible replacements.
local escaped_from = to
-- Handling of '(rr)', '(r)', '.' and '-' needs to be done before calling pattern_escape(); otherwise they will be
-- escaped.
escaped_from = escaped_from:gsub("%(rr%)", "r")
escaped_from = escaped_from:gsub("%(r%)", "r")
escaped_from = escaped_from:gsub("ks", "x"):gsub("Ks", "X"):gsub("gz", "x"):gsub("([bg])%1l", "%1l"):gsub("[._]", "")
escaped_from = pattern_escape(escaped_from)
escaped_from = escaped_from:gsub("rr", "rr?")
escaped_from = escaped_from:gsub("ss", "ss?")
escaped_from = escaped_from:gsub("ʃ", "[xX]")
-- This is tricky, because we already passed `escaped_from` through pattern_escape() causing a hyphen to get a
-- % sign before it, and have to double up the percent signs to match and replace a literal %.
escaped_from = escaped_from:gsub("%%%-", "%%-?")
-- Tie sign (‿) should match against space, hyphen or nothing in the original.
escaped_from = escaped_from:gsub("‿", "[ %%-]?")
escaped_from = rsub(escaped_from, "[" .. written_accented_vowel_l .. "]",
function(v) return "[" .. v .. written_accented_to_plain_vowel[v] .. "]" end)
escaped_from = escaped_from:gsub(DOTOVER, DOTOVER .. "?"):gsub(LINEUNDER, LINEUNDER .. "?")
escaped_from = "(" .. escaped_from .. ")"
if whole_word then
escaped_from = "%f[%a]" .. escaped_from .. "%f[%A]"
end
local match = umatch(pagename, escaped_from)
if match then
if match == to then
error(("Single substitution spec '%s' found in pagename '%s', replacement would have no effect"):
format(to, pagename))
end
return match
end
error(("Single substitution spec '%s' couldn't be matched to pagename '%s'"):format(to, pagename))
end
local function apply_substitution_spec(respelling, pagename, pos, allow_mid_vowel_hints, parse_err)
local subs = split_on_comma(umatch(respelling, "^%[(.*)%]$"))
respelling = pagename
local mid_vowel_hint
local regular_subs = {}
for _, sub in ipairs(subs) do
if ufind(sub, "^" .. export.mid_vowel_hint_c .. "$") then
if mid_vowel_hint then
parse_err(("Specified mid vowel hint twice, '%s' and '%s'"):format(
mid_vowel_hint, sub))
end
mid_vowel_hint = sub
else
table.insert(regular_subs, sub)
end
end
if mid_vowel_hint then
if not allow_mid_vowel_hints then
parse_err(("Mid vowel hint '%s' not allowed when apply one substitution spec to multiple words"):format(
mid_vowel_hint))
end
local suffix = ""
-- FIXME: This duplicates logic in to_IPA().
if not pos or pos == "adverb" then
local part_before_ment, ment = umatch(respelling, "^(.*)(m[eé]nt)$")
if part_before_ment and (pos == "adverb" or not ufind(part_before_ment, "[iï]$") and
ufind(part_before_ment, V .. ".*" .. V)) then
suffix = ment
respelling = part_before_ment
end
end
local syllables = split_syllables(respelling, "stress prefixes", "may be uppercase")
local stressed_vowel = syllables[syllables.stress].vowel
if stressed_vowel == mid_vowel_hint then
-- do nothing
elseif ufind(mid_vowel_hint, "[èéêë]") and ufind(stressed_vowel, "[eEèÈ]") or
ufind(mid_vowel_hint, "[òóô]") and ufind(stressed_vowel, "[oO]") then
syllables[syllables.stress].vowel = mid_vowel_hint
else
parse_err(("Stressed vowel '%s' not compatible with mid vowel hint '%s'"):format(
stressed_vowel, mid_vowel_hint))
end
respelling = reconstitute_word_from_syllables(syllables) .. suffix
end
for _, sub in ipairs(regular_subs) do
local from, escaped_from, to, escaped_to, whole_word
if ufind(sub, "^~") then
-- whole-word match
sub = umatch(sub, "^~(.*)$")
whole_word = true
end
if sub:find(":") then
from, to = umatch(sub, "^(.-):(.*)$")
else
to = sub
from = convert_single_substitution_to_original(to, pagename, whole_word)
end
if from then
escaped_from = pattern_escape(from)
if whole_word then
escaped_from = "%f[%a]" .. escaped_from .. "%f[%A]"
end
escaped_to = replacement_escape(to)
local subbed_respelling, nsubs = ugsub(respelling, escaped_from, escaped_to)
if nsubs == 0 then
parse_err(("Substitution spec %s -> %s didn't match processed pagename '%s'"):format(
from, to, respelling))
elseif nsubs > 1 then
parse_err(("Substitution spec %s -> %s matched multiple substrings in processed pagename '%s', add " ..
"more context"):format(from, to, respelling))
else
respelling = subbed_respelling
end
end
end
return respelling
end
local canonicalize_pos = {
n = "noun",
noun = "noun",
v = "verb",
vb = "verb",
verb = "verb",
a = "adjective",
adj = "adjective",
adjective = "adjective",
av = "adverb",
adv = "adverb",
adverb = "adverb",
o = "other",
other = "other",
}
local function parse_off_pos(respelling, parse_err)
local pos, rest = respelling:match("^([a-z]+)/(.*)$")
if pos then
if not canonicalize_pos[pos] then
local valid_pos = {}
for vp, _ in pairs(canonicalize_pos) do
table.insert(valid_pos, vp)
end
table.sort(valid_pos)
parse_err(("Unrecognized part of speech '%s', should be one of %s"):format(pos,
table.concat(valid_pos, ", ")))
end
pos = canonicalize_pos[pos]
respelling = rest
if respelling == "" then
respelling = "+"
end
end
return pos, respelling
end
-- Parse a respelling given by the user, allowing for '+' for pagename, mid vowel hints in place of a respelling and
-- substitution specs like '[ks]' or [val:vol,ê,ks]. In general, return an object {words = {WORD, WORD, ...}} where
-- WORD is of the form {term = PARSED_RESPELLING, pos = POS}. Other fields are set in special cases: If a raw respelling
-- was seen, the fields `raw_phonemic` and/or `raw_phonetic` are set; if '?' is seen, the field `unknown` is set; and if
-- '-' is seen, the field `omitted` is set.
local function parse_respelling(respelling, pagename, parse_err)
if respelling == "?" then
return {
unknown = true
}
end
if respelling == "-" then
return {
omitted = true
}
end
local saw_raw
local remaining_respelling = respelling:match("^raw:(.*)$")
if remaining_respelling then
saw_raw = true
respelling = remaining_respelling
end
local raw_phonemic, raw_phonetic = respelling:match("^/(.*)/ %[(.*)%]$")
if not raw_phonemic then
raw_phonemic = respelling:match("^/(.*)/$")
end
if not raw_phonemic and saw_raw then
raw_phonetic = respelling:match("^%[(.*)%]$")
end
if raw_phonemic or raw_phonetic then
return {
raw_phonemic = raw_phonemic,
raw_phonetic = raw_phonetic,
}
end
pagename = decompose_respelling(pagename)
respelling = decompose_respelling(respelling)
local function split_respelling_into_words(respelling, parse_pos)
respelling = canon_respelling(respelling)
local word_objs = {}
local respelling_words = split(respelling, " ")
for _, word in ipairs(respelling_words) do
local pos
if parse_pos then
pos, word = parse_off_pos(word, parse_err)
end
table.insert(word_objs, {term = word, pos = pos})
end
return {words = word_objs}
end
local function substitute_respelling_word(respelling_word, pagename_word)
local pos
pos, respelling_word = parse_off_pos(respelling_word, parse_err)
if respelling_word == "+" then
respelling_word = pagename_word
else
if ufind(respelling_word, "^" .. export.mid_vowel_hint_c .. "$") then
respelling_word = "[" .. respelling_word .. "]"
end
if ufind(respelling_word, "^%[.*%]$") then
respelling_word = apply_substitution_spec(respelling_word, pagename_word, pos,
"allow mid vowel hint", parse_err)
end
end
return {term = respelling_word, pos = pos}
end
-- At this point, if there are multiple words in the pagename, there are three syntaxes allowed: all-at-once,
-- replacement or word-by-word. All-at-once syntax involves either a + representing the entire pagename, or a
-- substitution spec that applies to all words in the pagename. This syntax cannot have a prefixed part of speech
-- because it wouldn't be clear which word to apply the part of speech to. Replacement syntax simply spells out the
-- respelling without any substitution specs or +'s (but possibly with parts of speech prefixed to individual
-- words), and can have a different number of words than the pagename (essentially, the pagename is disregarded).
-- Word-by-word syntax involves a combination of respelled words, per-word substitution specs and/or a +
-- representing an individual word, and must have the same number of words as the pagename so that substitution
-- specs and +'s can be lined up with words in the pagename. In all cases, the return value is in the same format;
-- see comment at top of function.
if pagename:find(" ") or respelling:find(" ") then
if respelling == "+" then
return split_respelling_into_words(pagename)
elseif ufind(respelling, "^%[.*%]$") then
-- all-at-once syntax with substitution spec
return split_respelling_into_words(apply_substitution_spec(respelling, pagename, nil, false, parse_err))
elseif ufind(respelling, "^([a-z]+)/$") or ufind(respelling, "^([a-z]+)/%[[^%[%]]*%]$") then
-- attempt to include a part of speech in all-at-once syntax
parse_err(("Part of speech not allowed when pagename is multiword and all-at-once syntax is used in " ..
"the respelling, but saw '%s'"):format(respelling))
elseif ufind(respelling, "^" .. export.mid_vowel_hint_c .. "$") then
-- attempt to use a mid-vowel hint in all-at-once syntax
parse_err(("Single mid-vowel hint not allowed when pagename is multiword because it's not clear which " ..
"word to apply it to, but saw '%s'"):format(respelling))
elseif ufind(respelling, "[+%[%]]") or ufind(respelling, "^" .. export.mid_vowel_hint_c .. " ") or
ufind(respelling, " " .. export.mid_vowel_hint_c .. " ") or
ufind(respelling, " " .. export.mid_vowel_hint_c .. "$") then
-- word-by-word syntax
local sub_with_space = umatch(respelling, "%[[^%[%]]* [^%[%]]*%]")
if sub_with_space then
parse_err(("When using word-by-word syntax with a multiword pagename, saw substitution spec '%s' " ..
"with spaces, which is not allowed because it must match a single word"):format(sub_with_space))
end
pagename = canon_respelling(pagename)
respelling = canon_respelling(respelling)
local pagename_words = split(pagename, " ")
local respelling_words = split(respelling, " ")
if #pagename_words ~= #respelling_words then
parse_err(("When using word-by-word syntax with a multiword pagename, saw %s words in pagename but " ..
"%s word%s in respelling; they need to match"):format(#pagename_words, #respelling_words,
#respelling_words > 1 and "s" or ""))
end
local word_objs = {}
for i = 1, #pagename_words do
table.insert(word_objs, substitute_respelling_word(respelling_words[i], pagename_words[i]))
end
return {words = word_objs}
else
-- replacement syntax; pagename ignored
return split_respelling_into_words(respelling, "parse pos")
end
else
local word_obj = substitute_respelling_word(respelling, pagename)
word_obj.term = canon_respelling(word_obj.term)
return {words = {word_obj}}
end
end
-- Parse a list of comma-split runs containing one or more respellings, i.e. after calling parse_balanced_segment_run()
-- or the like followed by split_alternating_runs() or the like (see [[Module:parse utilities]]). `pagename` is the
-- pagename, for use when a respelling is just '+', a mid-vowel hint like 'ê' or a substitution spec like '[ks]'.
-- `original_input` is the raw input and `input_param` the name of the param containing the raw input; both are used
-- only in error messages. Return an object specifying the respellings, currently with a single field 'terms' (this
-- format is used in case other outer properties exist in the future), where 'terms' is a list of term objects. Each
-- term object contains either a field `term` with the respelling and an optional part of speech `pos`, or fields
-- `raw_phonemic` and/or `raw_phonetic` (if the user specified raw IPA using "/.../" or "/.../ [...]" or "raw:[...]"),
-- `unknown` (if the user specified "?"), or `omitted` (if the user specified "-"). In addition, there may be fields
-- `q`, `qq`, `a`, `aa`, and/or `ref` corresponding to inline modifiers. Each such field is a list; all are lists of
-- strings except for `ref`, which is a list of objects as returned by parse_references() in [[Module:references]].
function export.parse_comma_separated_groups(comma_separated_groups, pagename, original_input, input_param)
local function generate_obj(respelling, parse_err)
return parse_respelling(respelling, pagename == true and respelling or pagename, parse_err)
end
local put = require(parse_utilities_module)
local outer_container = {terms = {}}
for _, group in ipairs(comma_separated_groups) do
-- Rejoin runs that don't involve <...>.
local j = 2
while j <= #group do
if not group[j]:find("^<.*>$") then
group[j - 1] = group[j - 1] .. group[j] .. group[j + 1]
table.remove(group, j)
table.remove(group, j)
else
j = j + 2
end
end
local param_mods = {
-- pre = { overall = true },
-- post = { overall = true },
ref = { store = "insert", convert = function(arg, parse_err)
return require("Module:references").parse_references(arg)
end },
q = { store = "insert" },
qq = { store = "insert" },
a = { store = "insert" },
aa = { store = "insert" },
}
table.insert(outer_container.terms, put.parse_inline_modifiers_from_segments {
group = group,
arg = original_input,
props = {
paramname = input_param,
param_mods = param_mods,
generate_obj = generate_obj,
splitchar = ",",
outer_container = outer_container,
},
})
end
return outer_container
end
-- Generate the pronunciation of `words` (a list of word objects representing respellings, each of which is an object
-- of the form {term = RESPELLING, pos = PART_OF_SPEECH} in `dialect` ("cen", "bal" or "val").
local function to_IPA(words, dialect)
local pronuns = {}
for _, wordobj in ipairs(words) do
if ufind(wordobj.term, "[áìùÁÌÙ]") then
error(("Invalid accented character in respelling '%s'; use accented à í ú, not the reversed versions"
):format(wordobj.term))
end
end
words = handle_unstressed_words(words)
for _, wordobj in ipairs(words) do
local word = wordobj.term
local pos = wordobj.pos
local suffix_syllables = {}
local orig_word = word
word = ulower(word)
if not pos or pos == "adverb" then
local word_before_ment = umatch(word, "^(.*)(m[eé]nt)$")
if word_before_ment and (pos == "adverb" or not ufind(word_before_ment, "[iï]$") and
ufind(word_before_ment, V .. ".*" .. V)) then
suffix_syllables = {{onset = "m", vowel = "e", coda = "nt", stressed = true}}
pos = "adjective"
word = word_before_ment
end
end
word = word_fixes(word, dialect)
local syllables = split_syllables(word)
syllables = preprocess_word(syllables, suffix_syllables, dialect, pos, orig_word)
-- Combine syllables.
local combined = {}
local has_ment = #suffix_syllables > 0
for i, syll in ipairs(syllables) do
local ac = (i == syllables.stress and not syllables.is_prefix and not has_ment or
has_ment and i == #syllables) and AC or -- primary stress
syllables[i].stressed and GR or -- secondary stress
""
table.insert(combined, syll.onset .. syll.vowel .. ac .. syll.coda)
end
table.insert(pronuns, table.concat(combined, "."))
end
-- Put double ## at utterance boundaries (beginning/end of string) and at foot boundaries (marked with |).
-- Note that if the string without pound signs is 'foo bar baz | bat quux', the final string will be
-- '##foo# #bar# #baz## #|# ##bat# #quux##'.
local text = "##" .. table.concat(pronuns, " ") .. "##"
text = rsub(text, " | ", "# | #")
text = rsub(text, " ", "# #")
return postprocess_general(text, dialect)
end
-- Generate the phonemic and phonetic pronunciations of the respellings in `parsed_respellings`, which is a table whose
-- keys are dialect identifiers (e.g. "cen" for Central Catalan, "val" for Valencian) and whose values are objects of
-- the format returned by parse_comma_separated_groups() (see comment above that function). This destructively modifies
-- `parsed_respellings`, adding fields `phonemic` and `phonetic` containing the generated pronunciations and removing
-- the input fields used to generate those output fields. (FIXME: Currently only phonetic pronunciation is generated.)
function export.generate_phonemic_phonetic(parsed_respellings)
-- Convert each canonicalized respelling to phonemic/phonetic IPA.
for dialect, respelling_spec in pairs(parsed_respellings) do
for _, termobj in ipairs(respelling_spec.terms) do
if termobj.unknown or termobj.omitted then
-- leave alone, will handle later
elseif termobj.raw_phonemic or termobj.raw_phonetic then
termobj.phonemic = termobj.raw_phonemic
termobj.phonetic = termobj.raw_phonetic
-- set to nil so by-value comparisons respect only the resulting phonemic/phonetic and qualifiers
termobj.raw_phonemic = nil
termobj.raw_phonetic = nil
else
termobj.phonetic = to_IPA(termobj.words, dialect)
-- set to nil so by-value comparisons respect only the resulting phonemic/phonetic and qualifiers
termobj.words = nil
end
end
end
end
-- Group pronunciations by dialect, i.e. grouping pronunciations that are identical in every way (including both the
-- pronunciation(s) and any qualifiers and other inline modifiers). `parsed_respellings` contains the output from
-- generate_phonemic_phonetic(), and the return value is a list of grouped pronunciations, where each object in the list
-- contains fields `dialects` (a list of dialects containing the pronunciations) and `pronuns` (a list of
-- pronunciations, where each pronunciation is specified by an object containing fields `phonemic` and `phonetic`, as
-- generated by generate_phonemic_phonetic(), along with any inline modifier fields `q`, `qq`, `a`, `aa` and/or `ref`).
function export.group_pronuns_by_dialect(parsed_respellings)
local grouped_pronuns = {}
for dialect, pronun_spec in pairs(parsed_respellings) do
local saw_omitted = false
for _, termobj in ipairs(pronun_spec.terms) do
if termobj.omitted then
saw_omitted = true
break
end
end
if not saw_omitted then
local saw_existing = false
for _, group in ipairs(grouped_pronuns) do
if deep_equals(group.pronuns, pronun_spec.terms) then
table.insert(group.dialects, dialect)
saw_existing = true
break
end
end
if not saw_existing then
table.insert(grouped_pronuns, {dialects = {dialect}, pronuns = pronun_spec.terms})
end
end
end
return grouped_pronuns
end
-- Format pronunciations grouped by dialect. `grouped_pronuns` contains the output of group_pronuns_by_dialect().
-- This destructively modifies `grouped_pronuns`, adding a field 'formatted' to the first-level values of
-- `grouped_pronuns` containing the formatted pronunciation(s) for a given set of dialects.
function export.format_grouped_pronunciations(grouped_pronuns)
for _, grouped_pronun_spec in pairs(grouped_pronuns) do
local pronunciations = {}
-- Loop through each pronunciation. For each one, add the phonemic and phonetic versions to `pronunciations`,
-- for formatting by [[Module:IPA]] or raw (for use in [[Module:ca-headword]]).
for j, pronun in ipairs(grouped_pronun_spec.pronuns) do
-- Add dialect tags to left accent qualifiers if first one
local as = pronun.a
if j == 1 then
if as then
as = deep_copy(as)
else
as = {}
end
for _, dialect in ipairs(grouped_pronun_spec.dialects) do
table.insert(as, export.dialects_to_names[dialect])
end
end
local first_pronun = #pronunciations + 1
if pronun.unknown then
-- FIXME: This is a massive hack but it works for now.
table.insert(pronunciations, { pron = "", pretext = "''unknown''" })
else
if not pronun.phonemic and not pronun.phonetic then
error("Internal error: Saw neither phonemic nor phonetic pronunciation")
end
if pronun.phonemic then -- missing if 'raw:[...]' given
local slash_pron = "/" .. pronun.phonemic .. "/"
table.insert(pronunciations, {
pron = slash_pron,
})
end
if pronun.phonetic then -- missing if '/.../' given
local bracket_pron = "[" .. pronun.phonetic .. "]"
table.insert(pronunciations, {
pron = bracket_pron,
})
end
end
local last_pronun = #pronunciations
if pronun.q then
pronunciations[first_pronun].q = pronun.q
end
if as then
pronunciations[first_pronun].a = as
end
if j > 1 then
pronunciations[first_pronun].separator = ", "
end
if pronun.qq then
pronunciations[last_pronun].qq = pronun.qq
end
if pronun.aa then
pronunciations[last_pronun].aa = pronun.aa
end
if pronun.refs then
pronunciations[last_pronun].refs = pronun.refs
end
if first_pronun ~= last_pronun then
pronunciations[last_pronun].separator = " "
end
end
grouped_pronun_spec.formatted = format_IPA_full{
lang = lang,
items = pronunciations,
separator = "",
}
end
end
function export.show(frame)
local params = {
[1] = {},
indent = {},
pagename = {} -- for testing or documentation pages
}
for _, dialect in ipairs(export.dialects) do
params[dialect] = {}
end
for dialect_group, _ in pairs(export.dialect_groups) do
params[dialect_group] = {}
end
local args = require("Module:parameters").process(frame:getParent().args, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
-- Set inputs
local inputs = {}
-- If 1= specified, do all dialects.
if args[1] then
for _, dialect in ipairs(export.dialects) do
inputs[dialect] = {input = args[1], param = 1}
end
end
-- Then do dialect groups.
for dialect_group, group_dialects in pairs(export.dialect_groups) do
if args[dialect_group] then
for _, dialect in ipairs(group_dialects) do
inputs[dialect] = {input = args[dialect_group], param = dialect_group}
end
end
end
-- Then do individual dialect settings.
for _, dialect in ipairs(export.dialects) do
if args[dialect] then
inputs[dialect] = {input = args[dialect], param = dialect}
end
end
-- If no inputs given, set all dialects based on current pagename.
if not next(inputs) then
for _, dialect in ipairs(export.dialects) do
inputs[dialect] = {input = "+", param = "(pagename)"}
end
end
-- Parse the arguments.
local parsed_respellings = {}
for dialect, inputspec in pairs(inputs) do
local function generate_obj(respelling, parse_err)
return parse_respelling(respelling, pagename, parse_err)
end
if inputspec.input:find("[<%[]") then
local put = require(parse_utilities_module)
-- Parse balanced segment runs involving either [...] (substitution notation) or <...> (inline modifiers).
-- We do this because we don't want commas inside of square or angle brackets to count as respelling
-- delimiters. However, we need to rejoin square-bracketed segments with nearby ones after splitting
-- alternating runs on comma. For example, if we are given
-- "a[x]a<q:learned>,[vol:vôl,ks]<q:nonstandard>", after calling
-- parse_multi_delimiter_balanced_segment_run() we get the following output:
--
-- {"a", "[x]", "a", "<q:learned>", ",", "[vol:vôl,ks]", "", "<q:nonstandard>", ""}
--
-- After calling split_alternating_runs(), we get the following:
--
-- {{"a", "[x]", "a", "<q:learned>", ""}, {"", "[vol:vôl,ks]", "", "<q:nonstandard>", ""}}
--
-- We need to rejoin stuff on either side of the square-bracketed portions.
local segments = put.parse_multi_delimiter_balanced_segment_run(inputspec.input, {{"<", ">"}, {"[", "]"}})
local comma_separated_groups = put.split_alternating_runs_on_comma(segments)
-- Process each value.
local outer_container = export.parse_comma_separated_groups(comma_separated_groups, pagename,
inputspec.input, inputspec.param)
parsed_respellings[dialect] = outer_container
else
local termobjs = {}
local function parse_err(msg)
error(msg .. ": " .. inputspec.param .. "=" .. inputspec.input)
end
for _, term in ipairs(split_on_comma(inputspec.input)) do
table.insert(termobjs, generate_obj(term, parse_err))
end
parsed_respellings[dialect] = {
terms = termobjs,
}
end
end
-- Convert each canonicalized respelling to phonemic/phonetic IPA.
export.generate_phonemic_phonetic(parsed_respellings)
-- Group the results.
local grouped_pronuns = export.group_pronuns_by_dialect(parsed_respellings)
-- Format the results.
export.format_grouped_pronunciations(grouped_pronuns)
-- Concatenate formatted results.
local formatted = {}
for _, grouped_pronun_spec in ipairs(grouped_pronuns) do
table.insert(formatted, grouped_pronun_spec.formatted)
end
local indent = (args.indent or "*") .. " "
local out = table.concat(formatted, "\n" .. indent)
if args.indent then
out = indent .. out
end
return out
end
-- Used by [[Module:ca-IPA/testcases]].
function export.test(pagename, respelling, dialect)
local function parse_err(msg)
error(msg)
end
local parsed = parse_respelling(respelling, pagename, parse_err)
return to_IPA(parsed.words, dialect)
end
return export
o2kuone2oiebt96xplzd9wp906cdkyj
178548
178547
2026-10-29T01:02:17Z
GinawaSaHapon
3989
178548
Scribunto
text/plain
local export = {}
local ipa_module = "Module:IPA"
local languages_module = "Module:languages"
local parse_utilities_module = "Module:parse utilities"
local string_char_module = "Module:string/char"
local string_pattern_escape_module = "Module:string/patternEscape"
local string_replacement_escape_module = "Module:string/replacementEscape"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local m_table = require(table_module)
local deep_copy = m_table.deepCopy
local deep_equals = m_table.deepEquals
local list_to_set = m_table.listToSet
local toNFC = mw.ustring.toNFC
local u = require(string_char_module)
local lang = require(languages_module).getByCode("ca")
local function format_IPA_full(...)
format_IPA_full = require(ipa_module).format_IPA_full
return format_IPA_full(...)
end
local function pattern_escape(...)
pattern_escape = require(string_pattern_escape_module)
return pattern_escape(...)
end
local function replacement_escape(...)
replacement_escape = require(string_replacement_escape_module)
return replacement_escape(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function ufind(...)
ufind = require(string_utilities_module).find
return ufind(...)
end
local function ugcodepoint(...)
ugcodepoint = require(string_utilities_module).gcodepoint
return ugcodepoint(...)
end
local function ugsub(...)
ugsub = require(string_utilities_module).gsub
return ugsub(...)
end
local function ulower(...)
ulower = require(string_utilities_module).lower
return ulower(...)
end
local function umatch(...)
umatch = require(string_utilities_module).match
return umatch(...)
end
local function usub(...)
usub = require(string_utilities_module).sub
return usub(...)
end
--[=[
FIXME:
1. [zʒ] should reduce to [ʒ] in Central and Balearic ([[disjunt]], [[disjuntor]]). Similar for [sʃ]
([[desxifrar]]). [DONE]
2. There needs to be a way of forcing [ʃ]. (Maybe just ʃ?) [DONE]
3. Make sure manual dot for syllable break works, cf. [[best-seller]] respelled `bèst.sèlerr'. [DONE]
4. Explicit accents on a/i/u should be removed in split_syllables(). [NOT DONE; not needed]
5. Compress double schwa in Central/Balearic in e.g. [[sobreescalfament]], [[centreafricà]], [[contraatac]],
[[contraescarpa]]; seems not to operate in Valencian.
6. Compress unstressed <ie> and <oe> followed by coda consonant -> [u] in Central/Balearic in e.g. [[aeroespacial]],
[[autoescola]], [[antiespasmòdic]], but not [[autoerotisme]], [[antiemètic]]; seems not to operate in Valencian.
NOTE: It does operate in an open syllable in [[fotoelèctric]], [[fotoelectricitat]], [[macroeconomia]]; not sure why.
7. Compress unstressed <oo> followed by a coda consonant -> [u] in Central/Balearic in e.g. [[microorganisme]]. Seems
not to operate in Valencian.
8. bm -> [mm] e.g. [[subministrament]]; seems not to operate in Valencian.
9. ë (and presumably ê) doesn't work in secondary stress, always becomes /ɛ/ (e.g. in [[extrajudicial]] respelled
'ëxtrajudiciàl'; this seems to be because the handling of ë goes through mid_vowel_hint, which doesn't work for
secondary stress. [DONE]
10. Respect ʃ at beginning of word in Valencian. [DONE]
11. [ʃ] in single substitution specs should match against written x. [DONE]
12. Prefixes e.g. [[xilo-]] should not have stress by default, and written primary stresses should be converted to
secondary. [DONE]
13. Convert apostrophe near beginning to tie (‿) and make sure we take account of it later, so that words like
[[captindre's]] and phrases like [[dona d'aigua]] work correctly. [DONE]
14. Correctly handle -bl and -gl in respelling, generating [bl] and [gl]. [NOT DONE; use _bl, _gl]
15. Correctly handle [βðɣ] in respelling forcing fricatives; should not be fortitioned. [NOT DONE; not needed]
16. [βðɣ] in single substitution specs should match against b/d/g. [NOT DONE; not needed]
17. [ss] in single substitution specs should match against ss?; used to force a pronounced [s]. [DONE]
18. [dm] in single substitution specs should match against [td]m. [NOT DONE; not needed]
19. Correctly handle written -dg- after [rz]: fricatives in Valencian, stops in Central (and Balearic?). [DONE]
20. Correctly handle lenition of written -bdg-: (1) -b- not lenited in Valencian or Balearic, lenited to [β] in
Central Catalan after vowels and consonants except nasals and [rz]; (2) -g- not lenited after nasals, also not
after [rz] in Central Catalan (and maybe Balearic?), otherwise yes except utterance initial; (3) -d- not lenited
after nasals or laterals, also not after [rz] in Central Catalan (and maybe Balearic?), otherwise yes except
utterance initial. Verify against ca-IPA equivalent on cawikt and also based on {{w|Catalan phonology}} and the IEC
grammar that Vriullop linked. [DONE]
21. Finish rewriting do_dialect_specific() to operate on whole word using Lua patterns. [DONE]
22. Implement multiword handling. [DONE]
23. Make sure suffix handling works correctly. [DONE]
24. Add many more test cases and redo test harness ala the German test harness. [DONE]
25. Redo handling of mid-vowel hints so it gets done early and in one place. [DONE]
26. Think about how to solve the issue of mid-vowel hints along with secondary stress marks in substitution specs.
Maybe a single mid-vowel spec should be rewritten to be a single substitution spec and the insertion of the
mid-vowel spec should happen during resolution of substitution specs. [DONE]
27. <tm> should default to [dm] not [mm]. [DONE]
28. Fix handling of mid vowel default in -è/-ès/-esa so it doesn't affect [[tèbia]] etc. [DONE]
29. x- after hyphen should probably become tx- in Valencian, cf. [[para-xocs]]. [DONE]
30. Implement DOTOVER to indicate lack of stress in a word, e.g. in a suffix. [DONE]
31. Handle words without vowels. [DONE]
32. Finish reviewing places where we may need to check for tie symbols.
33. Handle tie indicating liaison in e.g. [[Sant Antoni de Portmany]]. [DONE]
34. Handle pronunciation of [[amb]] correctly. [DONE]
35. Handle tie indicating liaison before h- correctly, e.g. [[Sant Hipòlit]]. [DONE]
36. Lenition should happen in Valencian in [[regla]] whether respelled 'réggla', 'régla' or 'rég_la'. [DONE]
37. Syllabification should happen correctly when underscore is used in 'bíb_lia' to block doubling of <bl>. [DONE]
38. <cn> should show up as [ŋn]. [DONE]
39. Delete [t] after [s] before anything but [s] ([[best-seller]]) or [ɾ] ([[postrem]]). [DONE]
40. Delete <t/d> after <n> before consonant even in Valencian; likewise for <p/b> after <m>, <c/g> after <n>. [DONE]
41. DOTOVER in single substitution specs should work. [DONE]
42. Underline in single substitution specs should work. [DONE]
43. LINEUNDER should work to indicate secondary stress after the primary stress, including in single substitution
specs. [DONE]
]=]
export.dialects = {"bal", "cen", "val"}
export.dialects_to_names = {
bal = "Balearic",
cen = "Central Catalan",
val = "Valencian",
}
export.dialect_groups = {
east = {"bal", "cen"},
}
local written_unaccented_vowel_l = "aeiouyAEIOUY"
local written_stressed_vowel_l = "àèéêëíòóôúýÀÈÉÊËÍÒÓÔÚÝ"
local written_accented_not_stressed_vowel_l = "ïüÏÜ"
local written_accented_vowel_l = written_stressed_vowel_l .. written_accented_not_stressed_vowel_l
local ipa_vowel_l = "ɔɛə"
local written_vowel_l = written_unaccented_vowel_l .. written_accented_vowel_l
local vowel_l = written_vowel_l .. ipa_vowel_l
local V = "[" .. vowel_l .. "]"
local written_accented_to_plain_vowel = {
["à"] = "a",
["è"] = "e",
["é"] = "e",
["ê"] = "e",
["ë"] = "e",
["í"] = "i",
["ï"] = "i",
["ò"] = "o",
["ó"] = "o",
["ô"] = "o",
["ú"] = "u",
["ü"] = "u",
["ý"] = "y",
["À"] = "A",
["È"] = "E",
["É"] = "E",
["Ê"] = "E",
["Ë"] = "E",
["Í"] = "I",
["Ï"] = "I",
["Ò"] = "O",
["Ó"] = "O",
["Ô"] = "O",
["Ú"] = "U",
["Ü"] = "U",
["Ý"] = "Y",
}
local AC = u(0x0301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local CFLEX = u(0x0302) -- circumflex = ̂
local DOTOVER = u(0x0307) -- dot over = ̇
local DIA = u(0x0308) -- diaeresis = ̈
local LINEUNDER = u(0x0331) -- lineunder = ̱
local stress_l = AC .. GR
local stress_c = "[" .. stress_l .. "]"
local ipa_stress_l = "ˈˌ"
local ipa_stress_c = "[" .. ipa_stress_l .. "]"
local sylsep_l = "%-." -- hyphen included for syllabifying from spelling; FIXME: formerly included SYLDIV
local sylsep_c = "[" .. sylsep_l .. "]"
local tie_l = "‿'"
local tie_c = "[" .. tie_l .. "]"
local charsep_l = sylsep_l .. tie_l .. stress_l .. ipa_stress_l
local charsep_c = "[" .. charsep_l .. "]"
local wordsep_l = "# "
local separator_l = charsep_l .. wordsep_l
local separator_c = "[" .. separator_l .. "]"
local neg_guts_of_cons = vowel_l .. separator_l
local C = "[^" .. neg_guts_of_cons .. "]" -- consonant class including h
export.mid_vowel_hints = "éèêëóòô"
export.mid_vowel_hint_c = "[" .. export.mid_vowel_hints .. "]"
local TEMP_PAREN_R = u(0xFFF1)
local TEMP_PAREN_RR = u(0xFFF2)
-- Pseudo-consonant at the edge of prefixes ending in a vowel and suffixes beginning with a vowel; FIXME: not currently
-- used.
local PSEUDOCONS = u(0xFFF3)
-- local PREFIX_MARKER = u(0xFFF4) -- marker indicating a prefix so we can convert primary to secondary accents
local valid_onsets = list_to_set{
"b", "bl", "br",
"c", "cl", "cr",
"ç",
"d", "dj", "dr",
"f", "fl", "fr",
"g", "gl", "gr", "gu", "gü",
"h",
"i",
"j",
"k", "kl", "kr",
"l", "ll",
"m",
"n", "ny", "ñ",
"p", "pl", "pr",
"qu", "qü",
"r", "rr",
"s", "ss",
"t", "tg", "tj", "tr", "tx", "tz",
"u",
"v", "vl", "vr",
"w",
"x",
"ʃ", -- e.g. 'χruʃóf' respelling of [[Khrusxov]]
"χ", -- in case of respelling
"y",
"z",
}
local decompose_dotover = {
-- No composed i, u or U with DOTOVER.
["ȧ"] = "a" .. DOTOVER,
["ė"] = "e" .. DOTOVER,
["ȯ"] = "o" .. DOTOVER,
["ẏ"] = "y" .. DOTOVER,
["Ȧ"] = "A" .. DOTOVER,
["Ė"] = "E" .. DOTOVER,
["İ"] = "I" .. DOTOVER,
["Ȯ"] = "O" .. DOTOVER,
["Ẏ"] = "Y" .. DOTOVER,
}
local unstressed_words = list_to_set{
-- proclitic object pronouns
"em", "et", "es", "el", "la", "els", "les", "li", "ens", "us", "ho", "hi", "en",
-- enclitic object pronouns usually attach with hyphen to preceding verb but not always, cf. [[tant me fa]]
"me", "te", "se", "lo", "los", "nos", "vos", "ne",
-- contracted object pronouns and articles attached with apostrophe so no need to include
-- unstressed possessives
"mon", "ma", "mos", "mes", "ton", "ta", "tos", "tes", "son", "sa", "sos", "ses",
-- prepositions
"a", "de", "per", "amb", "ab", -- 'en' already included as proclitic object pronouns
-- prepositional contractions
"al", "als", "del", "dels", "pel", "pels",
-- articles 'el', 'la', 'els', 'les' already included as proclitic pronouns
-- personal articles
"na", -- 'en' already included above
-- indefinite articles
"un", "uns",
-- salat articles
"ets", "so", -- 'es' already included as proclitic object pronouns and 'ses', 'sa', 'sos' as possessives
-- conjunctions
"i", "o", "si", "ni", "que",
}
-- Version of ugsub() that discards all but the first return value.
local function rsub(term, foo, bar)
local retval = ugsub(term, foo, bar)
return retval
end
-- Version of ugsub() that returns a 2nd argument boolean indicating whether a substitution was made.
local function rsubb(term, foo, bar)
local retval, nsubs = ugsub(term, foo, bar)
return retval, nsubs > 0
end
-- Apply rsub() repeatedly until no change.
local function rsub_repeatedly(term, foo, bar)
while true do
local new_term = rsub(term, foo, bar)
if new_term == term then
return term
end
term = new_term
end
end
local function split_into_chars(text)
local chars = {}
for codepoint in ugcodepoint(text) do
table.insert(chars, u(codepoint))
end
return chars
end
local function split_on_comma(term)
if term:find(",%s") or term:find("\\") then
return require(parse_utilities_module).split_on_comma(term)
end
return split(term, ",")
end
local function concat_keys(tab)
local res = {}
for k, _ in pairs(tab) do
table.insert(res, k)
end
return table.concat(res)
end
local function handle_unstressed_words(words)
words = deep_copy(words)
-- Lowercase all words for ease in further processing.
for _, wordobj in ipairs(words) do
wordobj.term = ulower(wordobj.term)
end
-- Check if the word at index `i` in `words` is "amb" and the following word begins with a vowel.
local function is_amb_to_join(words, i)
return i < #words and words[i].term == "a" .. DOTOVER .. "mb" and ufind(words[i + 1].term, "^h?" .. V)
end
local saw_amb_to_join = true
-- Mark all unstressed words with DOTOVER, so that split_syllables() doesn't assign stress. We need to do this
-- before special handling for [[amb]], because [[amb]] may join to another unstressed word like [[el]], in the
-- process losing the identity of the two words. In the process, see if [[amb]] occurs before a following
-- vowel-initial word (which may begin with h-).
for i, wordobj in ipairs(words) do
-- Put DOTOVER after the last vowel (to handle the case of [[que]]). It doesn't actually matter where we put
-- it, because split_syllables() just looks for DOTOVER anywhere in the word.
if unstressed_words[wordobj.term] then
wordobj.term = rsub(wordobj.term, "^(.*" .. V .. ")", "%1" .. DOTOVER)
end
if is_amb_to_join(words, i) then
saw_amb_to_join = true
end
end
-- Join [[amb]] before vowel-initial word with following word.
if saw_amb_to_join then
local new_words = {}
local i = 1
while i <= #words do
if is_amb_to_join(words, i) then
table.insert(new_words, {term = words[i].term .. "‿" .. words[i + 1].term, pos = words[i + 1].pos})
i = i + 2
else
table.insert(new_words, words[i])
i = i + 1
end
end
words = new_words
end
-- Finally, rewrite some unstressed words to get the right pronunciation. Any remaining [[amb]] not before a
-- vowel-initial word is pronounced [am] even in Valencian (where [amp]/[amb] would be expected), and [[per]] always
-- has a pronounced <r>.
local unstressed_word_replacement = {
["a" .. DOTOVER .. "mb"] = "a" .. DOTOVER .. "m",
["pe" .. DOTOVER .. "r"] = "pe" .. DOTOVER .. "rr",
}
for _, wordobj in ipairs(words) do
wordobj.term = unstressed_word_replacement[wordobj.term] or wordobj.term
end
return words
end
local function fix_prefixes(word)
-- Voiced s in prefix roots -fons-, -dins-, -trans-
word = rsub(word, "^enfons([aàeèéiíoòóuú])", "enfonz%1")
word = rsub(word, "^endins([aàeèéiíoòóuú])", "endinz%1")
word = rsub(word, "tr([aà])ns([aàeèéiíoòóuúbdghlmv])", "tr%1nz%2")
-- in + ex > ineks/inegz
word = rsub(word, "^inex", "in.ex")
return word
end
local function restore_diaereses(word)
-- Some structural forms do not have diaeresis per diacritic savings, let's restore it to identify hiatus
word = rsub(word, "([iu])um(s?)$", "%1üm%2") -- Latinisms (-ius is ambiguous but rare)
word = rsub(word, "([aeiou])isme(s?)$", "%1ísme%2") -- suffix -isme
word = rsub(word, "([aeiou])ist([ae]s?)$", "%1íst%2") -- suffix -ista
word = rsub(word, "([aeou])ir$", "%1ír") -- verbs -ir
word = rsub(word, "([aeou])int$", "%1ínt") -- present participle
word = rsub(word, "([aeo])ir([éà])$", "%1ïr%2") -- future
word = rsub(word, "([^gq]u)ir([éà])$", "%1ïr%2")
word = rsub(word, "([aeo])iràs$", "%1ïràs")
word = rsub(word, "([^gq]u)iràs$", "%1ïràs")
word = rsub(word, "([aeo])ir(e[mu])$", "%1ïr%2")
word = rsub(word, "([^gq]u)ir(e[mu])$", "%1ïr%2")
word = rsub(word, "([aeo])iran$", "%1ïran")
word = rsub(word, "([^gq]u)iran$", "%1ïran")
word = rsub(word, "([aeo])iria$", "%1ïria") -- conditional
word = rsub(word, "([^gq]u)iria$", "%1ïria")
word = rsub(word, "([aeo])ir(ie[sn])$", "%1ïr%2")
word = rsub(word, "([^gq]u)ir(ie[sn])$", "%1ïr%2")
return word
end
local function fix_y(word)
-- y > vowel i else consonant /j/, except ny
word = rsub(word, "ny", "ñ")
word = rsub(word, "y([^aeiouàèéêëíòóôúïü])", "i%1") -- vowel if not next to another vowel
word = rsub(word, "([^aeiouàèéêëíòóôúïü·%-%.])y", "%1i") -- excluding also syllables separators
return word
end
local function mid_vowel_fixes(word)
local function track_mid_vowel(vowel, cont)
require("Module:debug/track"){"ca-IPA/" .. vowel, "ca-IPA/" .. vowel .. "/" .. cont}
return true
end
local changed
-- final -el (not -ell) usually è but not too many cases
word, changed = rsubb(word, "e(nts?)$", "é%1")
if changed then
track_mid_vowel("e", "nt-nts")
end
word, changed = rsubb(word, "e(rs?)$", "é%1")
if changed then
track_mid_vowel("e", "r-rs")
end
word, changed = rsubb(word, "o(rs?)$", "ó%1")
if changed then
track_mid_vowel("o", "r-rs")
end
word, changed = rsubb(word, "è(s?)$", "ê%1")
if changed then
track_mid_vowel("è", "s-blank")
end
word, changed = rsubb(word, "e(s[oe]s)$", "ê%1")
if changed then
track_mid_vowel("e", "sos-sa-ses")
end
word, changed = rsubb(word, "e(sa)$", "ê%1")
if changed then
track_mid_vowel("e", "sos-sa-ses")
end
return word
end
local function word_fixes(word, dialect)
word = rsub(word, "%(rr%)", TEMP_PAREN_RR)
word = rsub(word, "%(r%)", TEMP_PAREN_R)
word = rsub(word, "%-([rs]?)", "-%1%1")
if dialect == "val" then
word = rsub(word, "%-x", "-tx")
end
word = rsub(word, "rç$", "rrs") -- silent r only in plurals -rs
word = fix_prefixes(word) -- internal pause after a prefix
word = restore_diaereses(word) -- no diaeresis saving
word = fix_y(word) -- ny > ñ else y > i vowel or consonant
word = mid_vowel_fixes(word)
-- all words in pn- (e.g. [[pneumotòrax]] and mn- (e.g. [[mnemònic]]) have silent p/m in both Central and Valencian
word = rsub(word, "^[pm]n", "n")
-- Respell ch + vowel as tx, before we remove other h's after consonants.
word = rsub(word, "ch(" .. V ..")", "tx%1")
-- Delete h after a consonant. This must happen here, before split_syllables(). We don't delete h after a vowel
-- yet because it indicates a hiatus.
word = rsub(word, "(" .. C .. ")h", "%1")
return word
end
local function split_vowels(vowels, saw_dotover, saw_lineunder)
local syllables = {{onset = "", vowel = usub(vowels, 1, 1), coda = "", separator = "", has_dotover = saw_dotover,
has_lineunder = saw_lineunder}}
vowels = usub(vowels, 2)
while vowels ~= "" do
local syll = {onset = "", vowel = "", coda = ""}
syll.onset, syll.vowel, vowels = umatch(vowels, "^([iu]?)(.)(.-)$")
table.insert(syllables, syll)
end
local count = #syllables
if count >= 2 and (syllables[count].vowel == "i" or syllables[count].vowel == "u") then
syllables[count - 1].coda = syllables[count].vowel
syllables[count] = nil
end
return syllables
end
-- Split the word into syllables. Return a list of syllable objects, each of which contains fields `onset`, `vowel`,
-- `coda`, `separator` (a user-specified syllable divider that goes before the syllable; one of '·', '-' or '.') and
-- `stressed` (a boolean indicating that the syllable is stressed). In addition, the list has fields `stress` (the
-- index of the syllable with primary stress) and `is_prefix` (true if the word is a prefix, i.e. it ends in '-').
-- Normally, prefixes are treated as unstressed if a stressed syllable isn't explicitly marked, but this can be
-- overridden with `stress_prefixes`, which causes the automatic stress-assignment algorithm to run for these terms.
local function split_syllables(word, stress_prefixes, may_be_uppercase)
local syllables = {}
local saw_dotover = false
local remainder = word
local is_prefix = false
if remainder:find("%-$") then -- prefix
is_prefix = true
remainder = remainder:gsub("%-$", "")
end
local is_suffix = false
if remainder:find("^%-") then -- suffix
is_suffix = true
remainder = remainder:gsub("^%-", "")
end
while remainder ~= "" do
local consonants, vowels
-- FIXME: Using C and V below instead of the existing patterns slows things down TREMENDOUSLY.
-- Not sure why.
local vowel_list = may_be_uppercase and "aeiouàèéêëíòóôúïüAEIOUÀÈÉÊËÍÒÓÔÚÏÜ" .. DOTOVER .. LINEUNDER or
"aeiouàèéêëíòóôúïü" .. DOTOVER .. LINEUNDER
consonants, remainder = umatch(remainder, "^([^" .. vowel_list .. "]*)(.-)$")
vowels, remainder = umatch(remainder, "^([" .. vowel_list .. "]*)(.-)$")
local this_saw_dotover = not not ufind(vowels, DOTOVER)
if this_saw_dotover then
saw_dotover = true
vowels = vowels:gsub(DOTOVER, "")
end
local this_saw_lineunder = not not ufind(vowels, LINEUNDER)
if this_saw_lineunder then
vowels = vowels:gsub(LINEUNDER, "")
end
if vowels == "" then
if #syllables > 0 then
syllables[#syllables].coda = syllables[#syllables].coda .. consonants
else
-- word without vowels, e.g. foot boundary |
table.insert(syllables, {onset = consonants, vowel = "", coda = "", separator = ""})
end
else
local onset = consonants
local first_vowel = usub(vowels, 1, 1)
if (ufind(onset, "[gqGQ]$") and (first_vowel == "ü" or (first_vowel == "u" and vowels ~= "u")))
or ((onset == "" or onset == "h" or onset == "H") and #syllables == 0 and
(first_vowel == "i" or first_vowel == "I") and (vowels ~= "i" and vowels ~= "I"))
then
onset = onset .. usub(vowels, 1, 1)
vowels = usub(vowels, 2)
end
local vsyllables = split_vowels(vowels, this_saw_dotover, this_saw_lineunder)
vsyllables[1].onset = onset .. vsyllables[1].onset
for _, s in ipairs(vsyllables) do
table.insert(syllables, s)
end
end
end
-- Shift over consonants from the onset to the preceding coda, until the syllable onset is valid
for i = 2, #syllables do
local current = syllables[i]
local previous = syllables[i-1]
while not (current.onset == "" or valid_onsets[rsub(rsub(current.onset, tie_c .. "[hH]?$", ""), "_", "")]) do
local letter = usub(current.onset, 1, 1)
current.onset = usub(current.onset, 2)
if ufind(letter, "[·%-%.]") then -- syllable separators
current.separator = letter
break
else
previous.coda = previous.coda .. letter
if ufind(letter, tie_c) then
break
end
end
end
end
-- Detect stress
for i, syll in ipairs(syllables) do
if ufind(syll.vowel, "^[" .. written_stressed_vowel_l .. "]$") then
syll.stressed = true
-- primary stress: the last one stressed without LINEUNDER
if not syll.has_lineunder then
syllables.stress = i
end
end
end
-- Assign default stress
if not syllables.stress and not saw_dotover and (stress_prefixes or not is_prefix) then
local count = #syllables
if count == 1 then
if syllables[1].vowel ~= "" then -- vowel-less words don't get stress
syllables.stress = 1
end
else
local final = syllables[count]
-- Take account of tie symbols (apostrophes and ‿).
if ufind(final.coda, "^[s" .. tie_l .. "]*$") or (ufind(final.coda, "^" .. tie_c .. "*n" .. tie_c .. "*$") and (
final.vowel == "e" or final.vowel == "i" or final.vowel == "ï")) then
syllables.stress = count - 1
else
syllables.stress = count
end
end
if syllables.stress then
syllables[syllables.stress].stressed = true
end
end
syllables.is_prefix = is_prefix
syllables.is_suffix = is_suffix
return syllables
end
local function reconstitute_word_from_syllables(syllables)
local parts = {}
local function ins(txt)
table.insert(parts, txt)
end
if syllables.is_suffix then
ins("-")
end
for _, syl in ipairs(syllables) do
ins(syl.separator)
ins(syl.onset)
ins(syl.vowel)
if syl.has_dotover then
ins(DOTOVER)
end
if syl.has_lineunder then
ins(LINEUNDER)
end
ins(syl.coda)
end
if syllables.is_prefix then
ins("-")
end
return table.concat(parts)
end
local function decompose_respelling(text)
local dotover_keys = concat_keys(decompose_dotover)
return rsub(text, "[" .. dotover_keys .. "]", decompose_dotover)
end
local function canon_respelling(text)
local function canon_spaces(text)
text = rsub(text, "%s+", " ")
text = rsub(text, "^ ", "")
text = rsub(text, " $", "")
return text
end
text = canon_spaces(text)
-- eliminate upside down punctuation
text = rsub(text, "[¡¿]", "")
-- eliminate utterance-final punctuation
text = rsub(text, "[!?.]$", "")
-- eliminate double and triple quotes
text = rsub(text, "''+", "")
-- Convert commas and em/en dashes to IPA foot boundaries; require a space after commas and en dashes (for the
-- latter, in particular, to avoid treating the en dash in 'Bose–Einstein condensate' as a foot boundary.
text = rsub(text, " *[,–] ", " | ")
text = rsub(text, " *[—] *", " | ")
-- ... in phrases like [[com es diu...en català]] and [[necessito ...]] become foot boundaries
text = rsub(text, " *%.%.%. *", " | ")
-- remaining commas and en dashes become spaces
text = rsub(text, "[,–]", " ")
-- may need to eliminate extraneous spaces again, e.g. if there was a space before or after an eliminated
-- punctuation mark
text = canon_spaces(text)
-- question mark or exclamation point in the middle of a sentence -> IPA foot boundary
text = rsub(text, "([^ ]) *[!?] *([^ ])", "%1 | %2")
return text
end
local IPA_vowels_central = {
["ê"] = "ɛ", ["ë"] = "ɛ", ["ô"] = "ɔ",
}
local IPA_vowels_balearic = {
["ê"] = "ə", ["ë"] = "ɛ", ["ô"] = "ɔ",
}
local IPA_vowels_valencian = {
["ê"] = "e", ["ë"] = "e", ["ô"] = "o",
}
local IPA_vowels = {
["à"] = "a",
["è"] = "ɛ", ["ê"] = "ɛ", ["ë"] = "ɛ", ["é"] = "e",
["í"] = "i", ["ï"] = "i",
["ò"] = "ɔ", ["ô"] = "ɔ", ["ó"] = "o",
["ú"] = "u", ["ü"] = "u",
}
local function replace_context_free(cons)
cons = rsub(cons, "ŀ", "l")
cons = rsub(cons, "r", "ɾ")
cons = rsub(cons, "ɾɾ", "r")
cons = rsub(cons, "ss", "s")
cons = rsub(cons, "ll", "ʎ")
cons = rsub(cons, "ñ", "ɲ") -- hint ny > ñ
-- NOTE: We use single-character affricate symbols during processing for ease in handling, and convert them
-- to tied multi-character affricates at the end of join_syllables().
cons = rsub(cons, "[dt]j", "ʤ")
cons = rsub(cons, "tx", "ʧ")
cons = rsub(cons, "[dt]z", "ʣ")
cons = rsub(cons, "ç", "s")
cons = rsub(cons, "[cq]", "k")
cons = rsub(cons, "h", "")
cons = rsub(cons, "j", "ʒ")
-- Don't replace x -> ʃ yet so we can distinguish x from manually specified ʃ.
cons = rsub(cons, "i", "j") -- must be after j > ʒ
cons = rsub(cons, "y", "j") -- must be after j > ʒ and fix_y
cons = rsub(cons, "[uü]", "w")
cons = rsub(cons, "'", "‿")
return cons
end
-- Do context-sensitive phonological changes. Formerly this was all done syllable-by-syllable but that made the code
-- tricky (since it often had to look at adjacent syllables) and full of subtle bugs. Now we first concatenate the
-- syllables back to words and the words to the combined text and work on the text as a whole. FIXME: We should move
-- more of the work done in preprocess_word(), e.g. most of replace_context_free(), here.
local function postprocess_general(text, dialect)
local function verify(cond, msg)
if not cond then
error(("Internal error: %s; processed respelling at this point is '%s'"):format(msg, text))
end
return true
end
local voiced = list_to_set{"b", "d", "g", "m", "n", "ɲ", "l", "ʎ", "r", "ɾ", "v", "z", "ʒ", "ʣ", "ʤ"}
local voiceless = list_to_set{"p", "t", "k", "f", "s", "ʃ", "ʦ", "ʧ"}
local voicing = {["p"] = "b", ["t"] = "d", ["k"] = "g", ["f"] = "v", ["s"] = "z", ["ʃ"] = "ʒ", ["ʦ"] = "ʤ",
["ʧ"] = "ʤ"}
local devoicing = {}
for k, v in pairs(voicing) do
devoicing[v] = k
end
------------------ Handle <x>
-- Handle ex- + vowel > -egz-. We handle -x- on either side of the syllable boundary. Note that this also handles
-- inex- + vowel because in fix_prefixes we respell inex- as in.ex-, which ends up at this stage as in.e.xV.
text = rsub_repeatedly(text, "([.#][eɛ]" .. stress_c .. "*)(" .. charsep_c .. "*)x(" .. charsep_c .. "*" .. V ..
")", function(e, pre, post)
-- Preserve other character separators (especially the tie character ‿).
pre = pre:gsub("%.", "")
post = post:gsub("%.", "")
return e .. pre .. "g.z" .. post
end)
-- -x- at the beginning of a coda becomes [ks], e.g. [[annex]], [[apèndix]], [[extracció]]; but not elsewhere in
-- the coda, e.g. in [[romanx]], [[ponx]]; words with [ks] in -nx such as [[esfinx]], [[linx]], [[manx]] need
-- respelling with [ks]; words ending in vowel + x like [[ídix]] need respelling with [ʃ]
text = rsub(text, "(" .. V .. stress_c .. "*)x", "%1ks")
if dialect == "val" then
-- Word-initial <x> as well as <x> after a consonant other than /j/ (including in the coda, e.g. [[ponx]])
-- becomes [t͡ʃ].
text = rsub(text, "#x", "#ʧ")
text = rsub(text, "([^" .. vowel_l .. separator_l .. "j]" .. charsep_c .. "*)x", "%1ʧ")
end
-- Other x becomes [ʃ]
text = rsub(text, "x", "ʃ")
-- Doubled ss -> s e.g. in exs-, exc(e/i)-, sc(e/i)-; FIXME: should this apply across word boundaries?
text = rsub(text, "s(" .. charsep_c .. "*)s", "%1s")
------------------ Coda consonant losses
-- In Central Catalan, coda losses happen everywhere, but otherwise they don't happen when
-- absolutely word-finally before a vowel or end of utterance (e.g. [[blanc]] has /k/ in Balearic and
-- Valencian but not [[blancs]]). Must precede consonant assimilations.
local boundary = dialect == "cen" and "(.)" or "([^#])"
text = rsub(text, "m[pb]" .. boundary, "m%1")
text = rsub(text, "([ln])[td]" .. boundary, "%1%2")
text = rsub(text, "[nŋ][kg]" .. boundary, "ŋ%1")
if dialect == "val" or dialect == "bal" then
local before_cons = "(" .. separator_c .. "*" .. C .. ")"
text = rsub(text, "m[pb]" .. before_cons, "m%1")
text = rsub(text, "([ln])[td]" .. before_cons, "%1%2")
text = rsub(text, "[nŋ][kg]" .. before_cons, "ŋ%1")
end
-- Delete /t/ between /s/ and any consonant other than /s/ or /ɾ/. Must precede voicing assimilation and
-- t + lateral/nasal assimilation.
text = rsub(text, "st(" .. sylsep_c .. "*[^" .. neg_guts_of_cons .. "sɾ])", "s%1")
------------------ Consonant assimilations
if dialect == "cen" then
-- v > b in onsets (not in codas, e.g. [[ovni]] [ɔ́vni] and [[hafni]] [ávni]). This needs to precede
-- assimilation of nb -> mb.
text = rsub(text, "v(" .. C .. "*" .. V ..")", "b%1")
end
-- t + lateral assimilation -> geminate across syllable boundary. We don't any more do t + nasal assimiation
-- because there are too many exceptions, e.g. [[aritmètic]], [[atmosfèric]], [[ètnia]]. Instead, we require that
-- cases where it does happen use respelling to effect this. FIXME: this doesn't always happen in -tl- either,
-- e.g. [[atlàntic]] has [əllántik] in GDLC but [adlántik] in DNV.
--
-- FIXME: Clean this up, maybe move below voicing assimilation, investigate whether it operates across words,
-- move stuff below that special-cases tll in Valencian here.
text = rsub(text, "t(" .. sylsep_c .. ")([lʎ])", "%2%1%2")
-- n + labial > labialized assimilation
text = rsub(text, "n(" .. separator_c .. "*[mbp])", "m%1")
text = rsub(text, "n(" .. separator_c .. "*[fv])", "ɱ%1")
-- n + velar > velarized assimilation
text = rsub(text, "n(" .. separator_c .. "*[kg])", "ŋ%1")
-- l/n + palatal > palatalized assimilation
text = rsub(text, "([ln])(" .. separator_c .. "*[ʎɲʃʒʧʤ])", function(ln, palatal)
ln = ({["l"] = "ʎ", ["n"] = "ɲ"})[ln]
return ln .. palatal
end)
-- ɲs > ɲʃ; FIXME: not sure the purpose of this; it doesn't apply in [[menys]] or derived terms like [[menyspreu]]
-- NOTE: Per [https://giec.iec.cat/textgramatica/codi/4.4], it does apply in these scenarios but the result is
-- somewhere between [s] and [ʃ], which is why it isn't shown in GDLC.
-- text = rsub(text, "ɲs", "%1ʃ")
------------------ Handle <r>
-- In replace_context_free(), we converted single r to ɾ and double rr to r.
if dialect == "cen" then
text = rsub(text, TEMP_PAREN_R, "")
text = rsub(text, TEMP_PAREN_RR, "r")
elseif dialect == "bal" then
text = rsub(text, TEMP_PAREN_R, "")
text = rsub(text, TEMP_PAREN_RR, "")
else
verify(dialect == "val", ("Unrecognized dialect '%s'"):format(dialect))
text = rsub(text, TEMP_PAREN_R, "ɾ")
text = rsub(text, TEMP_PAREN_RR, "ɾ")
end
if dialect ~= "val" then
-- Coda /ɾ/ -> /r/
-- FIXME: This is inherited from the older code. Correct?
text = rsub(text, "(" .. V .. stress_c .. "*" .. C .. "*)ɾ", "%1r")
end
-- ɾ -> r word-initially or after [lns]; needs to precede voicing assimilation as <s> will be voiced to [z] before
-- /ɾ/.
text = rsub(text, "([#lns]" .. sylsep_c .. "*)ɾ", "%1r")
------------------ Voicing assimilation
-- Voicing or devoicing; we want to proceed from right to left, and due to the limitations of patterns (in
-- particular, the lack of support for alternations), it's difficult to do this cleanly using Lua patterns, so we
-- do it character by character.
local chars = split_into_chars(text)
-- We need to look two characters ahead in some cases, so start two characters from the end. This is safe because
-- the overall respelling ends in "##". (Similarly, as an optimization, don't check the first two characters, which
-- are always "##".)
for i = #chars - 2, 3, -1 do
-- We are looking for two consonants next to each other, possibly separated by a syllable or word divider.
-- We also handle a consonant followed by a syllable divider then a vowel, and a consonant word-finally.
-- Note that only coda consonants can change voicing, so we need to check to make sure we're in the coda.
local first = chars[i]
-- If `second` is nil, no assimilation occurs. Otherwise, `second` should be a consonant or empty string (which
-- represents a syllable or word boundary followed by a vowel or end of string), and we assimilate to that
-- consonant (empty string forces devoicing).
local second
-- If set to true, we're processing a consonant directly before a word boundary followed by a word beginning
-- with a vowel. In this context, voiceless sibilants voice. Note that we handle voicing of <s> word-internally
-- separately, in preprocess_word() [FIXME: maybe move much of the processing in preprocess_word() into this
-- function].
local word_boundary_before_vowel
if not ufind(first, C) then
-- leave `second` at nil; no assimilation
elseif chars[i + 1] == "#" then -- word boundary
if chars[i + 2] == " " then
-- chars[i + 3] should always be "#"
verify(chars[i + 3] == "#", "Word boundary followed by space but not #")
if ufind(chars[i + 4], C) then
second = chars[i + 4]
else
second = ""
word_boundary_before_vowel = true
end
else
second = ""
end
elseif ufind(chars[i + 1], sylsep_c) then -- syllable boundary
if ufind(chars[i + 2], C) then
second = chars[i + 2]
else
second = ""
end
elseif ufind(chars[i + 1], C) then
second = chars[i + 1]
else
-- followed by a vowel not across a syllable or word boundary; leave `second` as nil, no assimilation
end
if second then
-- Make sure we're in the coda. We have to look backwards until we find a vowel or syllable/word boundary.
local in_coda = false
local j = i - 1
while true do
verify(j > 0, "Missing word boundary at beginning of overall respelling")
if ufind(chars[j], "[" .. sylsep_l .. wordsep_l .. "]") then
break
elseif ufind(chars[j], V) then
in_coda = true
break
end
j = j - 1
end
if in_coda then
if word_boundary_before_vowel and ufind(first, "[zʒʣʤ]") then
-- leave alone
elseif voiced[second] and voicing[first] or word_boundary_before_vowel and ufind(first, "[sʃʦʧ]") then
chars[i] = voicing[first]
elseif (voiceless[second] or second == "") and devoicing[first] then
chars[i] = devoicing[first]
end
end
end
end
text = table.concat(chars)
-- gn -> ŋn e.g. [[regnar]] (including word-initial gn- e.g. [[gnòmic]], [[gneis]])
-- FIXME: This should be moved below voicing assimilation, and we need to investigate if it operates across words
-- (here I'm guessing yes).
if dialect ~= "cen" then
text = rsub(text, "#gn", "#n")
end
text = rsub(text, "g(" .. separator_c .. "*n)", "ŋ%1")
-- gʒ > d͡ʒ
-- FIXME: We need to investigate if it operates across words
text = rsub(text, "g(" .. sylsep_c .. "*)ʒ", "%1ʤ")
-- sʃ -> ʃ ([[desxifrar]]), zʒ -> ʒ ([[disjuntor]])
if dialect ~= "val" then
text = rsub(text, "s(" .. separator_c .. "*ʃ)", "%1")
text = rsub(text, "z(" .. separator_c .. "*ʒ)", "%1")
end
------------------ Gemination of <bl>, <gl>
if dialect ~= "val" then
-- bl -> bbl, gl -> ggl after the stress when following a vowel; to avoid this, use <b_l> or <g_l>.
-- This must follow v > b above. To force a hard ungeminated [b] or [g], use <_b> or <_g>.
text = rsub(text, "(" .. stress_c .. ")(" .. sylsep_c .. ")([bg])l", "%1%3%2%3l")
else -- Valencian; undo manually written 'bbl', 'ggl' in words like [[poblar]], [[reglament]]
text = rsub(text, "([bg])(" .. sylsep_c .. ")%1l", "%2%1l")
end
------------------ Lenition of voiced stops
-- In Central Catalan, b/d/g become fricatives (actually approximants, like in Spanish) in the onset following a
-- vowel and (except for <d>) after <l> and <ll> (cf. GDLC [[cabellblanc]] [kəβɛ̀ʎβláŋ]). This also happens across
-- word boundaries but doesn't happen after stops, nor in Central Catalan after [r], [ɾ] or [z] (and hence probably
-- not after [ʒ] either, although I can't find any examples in GDLC).
--
-- In Valencian, <b> doesn't lenite (at least formally?), but <d> and <g> do lenite after [r], [ɾ] or [z].
--
-- Balearic is like Valencian in not leniting <b>, and probably like Central Catalan otherwise.
local lenite_bdg = {["b"] = "β", ["d"] = "ð", ["g"] = "ɣ"}
if dialect == "cen" then
text = rsub(text, "([" .. vowel_l .. "jwv]" .. separator_c .. "*[.#]" .. separator_c .. "*)([bdg])",
function(before, bdg) return before .. lenite_bdg[bdg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)([bg])",
function(before, bg) return before .. lenite_bdg[bg] end)
elseif dialect == "val" then
text = rsub(text, "([" .. vowel_l .. "jwvrɾzʣ]" .. separator_c .. "*[.#]" .. separator_c .. "*)([dg])",
function(before, dg) return before .. lenite_bdg[dg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)(g)",
function(before, g) return before .. lenite_bdg[g] end)
else
verify(dialect == "bal", ("Unrecognized dialect '%s'"):format(dialect))
text = rsub(text, "([" .. vowel_l .. "jwv]" .. separator_c .. "*[.#]" .. separator_c .. "*)([dg])",
function(before, dg) return before .. lenite_bdg[dg] end)
text = rsub(text, "([" .. vowel_l .. "lʎ]" .. separator_c .. "*[.#]" .. separator_c .. "*)(g)",
function(before, g) return before .. lenite_bdg[g] end)
end
------------------ Vowel reduction
-- Reduction of unstressed a,e in Central and Balearic (Eastern Catalan).
if dialect ~= "val" then
-- The following rules seem to apply, based on the old code:
-- (1) Stressed a and e are never reduced.
-- (2) Unstressed e directly following ə is not reduced.
-- (3) Unstressed e directly before written <a> or before /ɔ/ is not reduced.
-- (4) Written <ee> when both vowels precede the primary stress is reduced to [əə]. (This rule preempts #2.)
-- (5) Written <ee> when both vowels follow the primary stress isn't reduced at all.
-- Rule #2 in particular seems to require that we proceed left to right, which is how the old code was
-- implemented.
-- FIXME: These rules seem overly complex and may produce incorrect results in some circumstances.
local words = split(text, " ")
for j, word in ipairs(words) do
local chars = split_into_chars(word)
-- See above where voicing assimilation is handled. The overall respelling begins and ends in #, which we
-- can ignore. We need to look ahead three chars in some circumstances, but in all those circumstances we
-- shoudn't run off the end (and have assertions to check this).
local seen_primary_stress = false
for i = 2, #chars - 1 do
local this = chars[i]
if chars[i] == AC then
seen_primary_stress = true
end
if (this ~= "a" and this ~= "e") or ufind(chars[i + 1], stress_c) then
-- Not a/e, or a stressed vowel; continue
else
local reduction = true
local prev, prev_stress, nxt, nxt_stress
if not ufind(chars[i - 1], sylsep_c) then
prev = ""
else
prev = chars[i - 2] -- this should be non-nil as chars[i - 1] is a syllable separator (not #)
verify(prev, "Missing # at word boundary")
prev_stress = ""
if ufind(prev, stress_c) then
prev_stress = prev
prev = chars[i - 3]
-- As above; chars[i - 2] is a stress indicator (not #).
verify(prev, "Missing # at word boundary")
end
end
if not ufind(chars[i + 1], sylsep_c) then
nxt = ""
-- leave nxt at nil
else
nxt = chars[i + 2]
nxt_stress = chars[i + 3]
-- chars[i + 1] is a syllable separator, so chars[i + 2] should not be a word boundary, so
-- chars[i + 3] should exist.
verify(nxt and nxt_stress, "Syllable separator at word boundary or missing # at word boundary")
end
if this == "e" and ufind(prev, "ə") then
reduction = false
elseif this == "e" and ufind(nxt, "[aɔ]") then
reduction = false
elseif this == "e" and nxt == "e" and not ufind(nxt_stress, AC) then
-- FIXME: Check specifically for AC duplicates previous logic but is probably wrong or unnecessary.
if not seen_primary_stress then
chars[i + 2] = "ə"
else
reduction = false
end
end
if reduction then
chars[i] = "ə"
end
end
end
words[j] = table.concat(chars)
end
text = table.concat(words, " ")
end
if dialect == "cen" then
-- Reduction of unstressed o (not before w)
text = rsub(text, "o([^" .. stress_l .. "w])", "u%1")
elseif dialect == "bal" then
-- Reduction of unstressed o per vowel harmony: unstressed /o/ -> /u/ directly before stressed /i/ or /u/;
-- as a Lua pattern, o can be followed only by consonants and/or syllable separators (no vowels, stress marks
-- or word separators).
text = rsub(text, "o([^" .. vowel_l .. stress_l .. wordsep_l .. "]*[iu]" .. stress_c .. ")", "u%1")
end
-- Final losses.
text = rsub(text, "j(ʧs?#)", "%1") -- boigs /bɔt͡ʃ/
text = rsub(text, "([ʃʧs])s#", "%1#") -- homophone plurals -xs, -igs, -çs
if dialect ~= "val" then
-- Remove j before palatal obstruents
text = rsub(text, "j(" .. sylsep_c .. "*[ʃʒʧʤ])", "%1")
else -- Valencian
-- Fortition of palatal fricatives
text = rsub(text, "ʒ", "ʤ")
text = rsub(text, "(i" .. stress_c .. "*" .. sylsep_c .. ")ʣ", "%1z")
end
if dialect ~= "cen" then
-- No palatal gemination ʎʎ > ll or ʎ, in Valencian and Balearic.
-- FIXME: These conditions seem to be targeting specific words and should probably be fixed using respelling
-- instead.
text = rsub(text, "([bpw]a" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "([mv]e" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(ti" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(m[oɔ]" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "(u" .. stress_c .. "*)ʎ(" .. sylsep_c .. "*)ʎ", "%1l%2l")
text = rsub(text, "ʎ(" .. sylsep_c .. "*ʎ)", "%1")
end
---------- Convert pseudo-symbols to real ones.
-- Convert g to IPA ɡ.
text = rsub(text, "g", "ɡ")
-- Convert pseudo-afficate symbols to full affricates.
local full_affricates = { ["ʦ"] = "t͡s", ["ʣ"] = "d͡z", ["ʧ"] = "t͡ʃ", ["ʤ"] = "d͡ʒ" }
text = rsub(text, "([ʦʣʧʤ])", full_affricates)
---------- Generate IPA stress marks.
-- Convert acute and grave to IPA stress marks.
text = rsub(text, AC, "ˈ")
text = rsub(text, GR, "ˌ")
-- Move IPA stress marks to the beginning of the syllable.
text = rsub_repeatedly(text, "([#.])([^#.]*)(" .. ipa_stress_c .. ")", "%1%3%2")
-- Suppress syllable divider before IPA stress indicator.
text = rsub(text, "%.(#?" .. ipa_stress_c .. ")", "%1")
-- Make all primary stresses but the last one in a given word be secondary. May be fed by the first rule above.
-- FIXME: Currently this is handled earlier, but we might want to move it here, as is done in [[Module:pt-pronunc]].
-- text = rsub_repeatedly(text, "ˈ([^ ]+)ˈ", "ˌ%1ˈ")
-- Make primary stresses in prefixes become secondary. (FIXME: Handled earlier now.)
-- text = rsub_repeatedly(text, "ˈ([^#]*#" .. PREFIX_MARKER .. ")", "ˌ%1")
-- Remove # symbols at word/text boundaries, as well as _ (which forces separate interpretation), pseudo-consonant
-- markers (at edges of some prefixes/suffixes), and prefix markers, and recompose.
text = rsub(text, "[#_" .. PSEUDOCONS .. "]", "")
text = toNFC(text)
return text
end
local function preprocess_word(syllables, suffix_syllables, dialect, pos, orig_word)
-- Stressed vowel is ambiguous
if syllables.stress then
local stressed_vowel = syllables[syllables.stress].vowel
if ufind(stressed_vowel, "[eo]") then
local marks = {["e"] = {AC, GR, CFLEX, DIA}, ["o"] = {AC, GR, CFLEX}}
local marked_vowels = {}
for _, mark in ipairs(marks[stressed_vowel]) do
table.insert(marked_vowels, stressed_vowel .. mark)
end
error(("In respelling '%s', the stressed vowel '%s' is ambiguous. Please mark it with an acute, " ..
"grave, or combined accent: %s."):format(orig_word, stressed_vowel,
mw.text.listToText(marked_vowels, nil, " or ")))
end
end
-- Final -r is ambiguous in many cases.
local final = syllables[#syllables]
-- Stressed final r after a or i in non-monosyllables is treated as (r), i.e. verbal infinitives are assumed (NOTE:
-- not always the case, e.g. there are many adjectives and nouns in -ar that should be marked as '(rr)', and
-- several loanword nouns in -ir that should be marked as 'rr'). Likewise for stressed final r or rs after é in
-- non-monosyllables (which are usually adjectives or nouns with the -er ending, but may be verbal infinitives,
-- which should be marked as 'ê(r)'). That is, it disappears other than in Valencian. All other final r and final
-- rs are considered ambiguous and need to be rewritten using rr, (rr) or (r).
if #syllables > 1 and final.stressed then
if final.coda == "r" and ufind(final.vowel, "[aàiíé]") or final.coda == "rs" and final.vowel == "é" or
final.vowel == "ó" and ufind(final.coda, "^rs?$") and ufind(final.onset, "[stdç]") then
final.coda = TEMP_PAREN_R
end
end
if ufind(final.coda, "^rs?$") or ufind(final.coda, "[^r]rs?$") then
error(("In respelling '%s', final -r by itself or in -rs is ambiguous except in the verbal endings -ar or " ..
"-ir, in the nominal or adjectival endings -er(s) and -[dtsç]or(s). In all other cases it needs to be " ..
"rewritten using one of 'rr' (pronounced everywhere), '(rr)' (pronounced everywhere but Balearic) or " ..
"'(r)' (pronounced only in Valencian). Note that adjectives in -ar usually need rewriting using '(rr)'; " ..
"nouns in -ar referring to places should be rewritten using '(r)'; and loanword nouns in -ir usually " ..
"need rewriting using 'rr'."):format(orig_word))
end
local syllables_IPA = {stress = syllables.stress, is_prefix = syllables.is_prefix, is_suffix = syllables.is_suffix}
for key, val in ipairs(syllables) do
syllables_IPA[key] = {onset = val.onset, vowel = val.vowel, coda = val.coda, stressed = val.stressed}
end
-- Replace letters with IPA equivalents
for i, syll in ipairs(syllables_IPA) do
-- Voicing of s
if syll.onset == "s" and i > 1 and ufind(syllables[i - 1].coda, "^[iu]?$") then
syll.onset = "z"
end
if ufind(syll.vowel, "^[eèéêëií]$") then
syll.onset = rsub(syll.onset, "tg$", "ʤ")
syll.onset = rsub(syll.onset, "[cg]$", {["c"] = "s", ["g"] = "ʒ"})
syll.onset = rsub(syll.onset, "[qg]u$", {["qu"] = "k", ["gu"] = "g"})
end
syll.coda = rsub(syll.coda, "igs?$", "iʤ")
syll.onset = replace_context_free(syll.onset)
syll.coda = replace_context_free(syll.coda)
syll.vowel = rsub(syll.vowel, ".",
dialect == "cen" and IPA_vowels_central or
dialect == "bal" and IPA_vowels_balearic or
IPA_vowels_valencian
)
syll.vowel = rsub(syll.vowel, ".", IPA_vowels)
end
for _, suffix_syl in ipairs(suffix_syllables) do
table.insert(syllables_IPA, suffix_syl)
end
return syllables_IPA
end
-- Given a single substitution spec, `to`, figure out the corresponding value of `from` used in a complete
-- substitution spec. `pagename` is the name of the page, either the actual one or taken from the `pagename` param.
-- `whole_word`, if set, indicates that the match must be to a whole word (it was preceded by ~).
local function convert_single_substitution_to_original(to, pagename, whole_word)
-- Replace specially-handled characters with a class matching the character and possible replacements.
local escaped_from = to
-- Handling of '(rr)', '(r)', '.' and '-' needs to be done before calling pattern_escape(); otherwise they will be
-- escaped.
escaped_from = escaped_from:gsub("%(rr%)", "r")
escaped_from = escaped_from:gsub("%(r%)", "r")
escaped_from = escaped_from:gsub("ks", "x"):gsub("Ks", "X"):gsub("gz", "x"):gsub("([bg])%1l", "%1l"):gsub("[._]", "")
escaped_from = pattern_escape(escaped_from)
escaped_from = escaped_from:gsub("rr", "rr?")
escaped_from = escaped_from:gsub("ss", "ss?")
escaped_from = escaped_from:gsub("ʃ", "[xX]")
-- This is tricky, because we already passed `escaped_from` through pattern_escape() causing a hyphen to get a
-- % sign before it, and have to double up the percent signs to match and replace a literal %.
escaped_from = escaped_from:gsub("%%%-", "%%-?")
-- Tie sign (‿) should match against space, hyphen or nothing in the original.
escaped_from = escaped_from:gsub("‿", "[ %%-]?")
escaped_from = rsub(escaped_from, "[" .. written_accented_vowel_l .. "]",
function(v) return "[" .. v .. written_accented_to_plain_vowel[v] .. "]" end)
escaped_from = escaped_from:gsub(DOTOVER, DOTOVER .. "?"):gsub(LINEUNDER, LINEUNDER .. "?")
escaped_from = "(" .. escaped_from .. ")"
if whole_word then
escaped_from = "%f[%a]" .. escaped_from .. "%f[%A]"
end
local match = umatch(pagename, escaped_from)
if match then
if match == to then
error(("Single substitution spec '%s' found in pagename '%s', replacement would have no effect"):
format(to, pagename))
end
return match
end
error(("Single substitution spec '%s' couldn't be matched to pagename '%s'"):format(to, pagename))
end
local function apply_substitution_spec(respelling, pagename, pos, allow_mid_vowel_hints, parse_err)
local subs = split_on_comma(umatch(respelling, "^%[(.*)%]$"))
respelling = pagename
local mid_vowel_hint
local regular_subs = {}
for _, sub in ipairs(subs) do
if ufind(sub, "^" .. export.mid_vowel_hint_c .. "$") then
if mid_vowel_hint then
parse_err(("Specified mid vowel hint twice, '%s' and '%s'"):format(
mid_vowel_hint, sub))
end
mid_vowel_hint = sub
else
table.insert(regular_subs, sub)
end
end
if mid_vowel_hint then
if not allow_mid_vowel_hints then
parse_err(("Mid vowel hint '%s' not allowed when apply one substitution spec to multiple words"):format(
mid_vowel_hint))
end
local suffix = ""
-- FIXME: This duplicates logic in to_IPA().
if not pos or pos == "adverb" then
local part_before_ment, ment = umatch(respelling, "^(.*)(m[eé]nt)$")
if part_before_ment and (pos == "adverb" or not ufind(part_before_ment, "[iï]$") and
ufind(part_before_ment, V .. ".*" .. V)) then
suffix = ment
respelling = part_before_ment
end
end
local syllables = split_syllables(respelling, "stress prefixes", "may be uppercase")
local stressed_vowel = syllables[syllables.stress].vowel
if stressed_vowel == mid_vowel_hint then
-- do nothing
elseif ufind(mid_vowel_hint, "[èéêë]") and ufind(stressed_vowel, "[eEèÈ]") or
ufind(mid_vowel_hint, "[òóô]") and ufind(stressed_vowel, "[oO]") then
syllables[syllables.stress].vowel = mid_vowel_hint
else
parse_err(("Stressed vowel '%s' not compatible with mid vowel hint '%s'"):format(
stressed_vowel, mid_vowel_hint))
end
respelling = reconstitute_word_from_syllables(syllables) .. suffix
end
for _, sub in ipairs(regular_subs) do
local from, escaped_from, to, escaped_to, whole_word
if ufind(sub, "^~") then
-- whole-word match
sub = umatch(sub, "^~(.*)$")
whole_word = true
end
if sub:find(":") then
from, to = umatch(sub, "^(.-):(.*)$")
else
to = sub
from = convert_single_substitution_to_original(to, pagename, whole_word)
end
if from then
escaped_from = pattern_escape(from)
if whole_word then
escaped_from = "%f[%a]" .. escaped_from .. "%f[%A]"
end
escaped_to = replacement_escape(to)
local subbed_respelling, nsubs = ugsub(respelling, escaped_from, escaped_to)
if nsubs == 0 then
parse_err(("Substitution spec %s -> %s didn't match processed pagename '%s'"):format(
from, to, respelling))
elseif nsubs > 1 then
parse_err(("Substitution spec %s -> %s matched multiple substrings in processed pagename '%s', add " ..
"more context"):format(from, to, respelling))
else
respelling = subbed_respelling
end
end
end
return respelling
end
local canonicalize_pos = {
n = "noun",
noun = "noun",
v = "verb",
vb = "verb",
verb = "verb",
a = "adjective",
adj = "adjective",
adjective = "adjective",
av = "adverb",
adv = "adverb",
adverb = "adverb",
o = "other",
other = "other",
}
local function parse_off_pos(respelling, parse_err)
local pos, rest = respelling:match("^([a-z]+)/(.*)$")
if pos then
if not canonicalize_pos[pos] then
local valid_pos = {}
for vp, _ in pairs(canonicalize_pos) do
table.insert(valid_pos, vp)
end
table.sort(valid_pos)
parse_err(("Unrecognized part of speech '%s', should be one of %s"):format(pos,
table.concat(valid_pos, ", ")))
end
pos = canonicalize_pos[pos]
respelling = rest
if respelling == "" then
respelling = "+"
end
end
return pos, respelling
end
-- Parse a respelling given by the user, allowing for '+' for pagename, mid vowel hints in place of a respelling and
-- substitution specs like '[ks]' or [val:vol,ê,ks]. In general, return an object {words = {WORD, WORD, ...}} where
-- WORD is of the form {term = PARSED_RESPELLING, pos = POS}. Other fields are set in special cases: If a raw respelling
-- was seen, the fields `raw_phonemic` and/or `raw_phonetic` are set; if '?' is seen, the field `unknown` is set; and if
-- '-' is seen, the field `omitted` is set.
local function parse_respelling(respelling, pagename, parse_err)
if respelling == "?" then
return {
unknown = true
}
end
if respelling == "-" then
return {
omitted = true
}
end
local saw_raw
local remaining_respelling = respelling:match("^raw:(.*)$")
if remaining_respelling then
saw_raw = true
respelling = remaining_respelling
end
local raw_phonemic, raw_phonetic = respelling:match("^/(.*)/ %[(.*)%]$")
if not raw_phonemic then
raw_phonemic = respelling:match("^/(.*)/$")
end
if not raw_phonemic and saw_raw then
raw_phonetic = respelling:match("^%[(.*)%]$")
end
if raw_phonemic or raw_phonetic then
return {
raw_phonemic = raw_phonemic,
raw_phonetic = raw_phonetic,
}
end
pagename = decompose_respelling(pagename)
respelling = decompose_respelling(respelling)
local function split_respelling_into_words(respelling, parse_pos)
respelling = canon_respelling(respelling)
local word_objs = {}
local respelling_words = split(respelling, " ")
for _, word in ipairs(respelling_words) do
local pos
if parse_pos then
pos, word = parse_off_pos(word, parse_err)
end
table.insert(word_objs, {term = word, pos = pos})
end
return {words = word_objs}
end
local function substitute_respelling_word(respelling_word, pagename_word)
local pos
pos, respelling_word = parse_off_pos(respelling_word, parse_err)
if respelling_word == "+" then
respelling_word = pagename_word
else
if ufind(respelling_word, "^" .. export.mid_vowel_hint_c .. "$") then
respelling_word = "[" .. respelling_word .. "]"
end
if ufind(respelling_word, "^%[.*%]$") then
respelling_word = apply_substitution_spec(respelling_word, pagename_word, pos,
"allow mid vowel hint", parse_err)
end
end
return {term = respelling_word, pos = pos}
end
-- At this point, if there are multiple words in the pagename, there are three syntaxes allowed: all-at-once,
-- replacement or word-by-word. All-at-once syntax involves either a + representing the entire pagename, or a
-- substitution spec that applies to all words in the pagename. This syntax cannot have a prefixed part of speech
-- because it wouldn't be clear which word to apply the part of speech to. Replacement syntax simply spells out the
-- respelling without any substitution specs or +'s (but possibly with parts of speech prefixed to individual
-- words), and can have a different number of words than the pagename (essentially, the pagename is disregarded).
-- Word-by-word syntax involves a combination of respelled words, per-word substitution specs and/or a +
-- representing an individual word, and must have the same number of words as the pagename so that substitution
-- specs and +'s can be lined up with words in the pagename. In all cases, the return value is in the same format;
-- see comment at top of function.
if pagename:find(" ") or respelling:find(" ") then
if respelling == "+" then
return split_respelling_into_words(pagename)
elseif ufind(respelling, "^%[.*%]$") then
-- all-at-once syntax with substitution spec
return split_respelling_into_words(apply_substitution_spec(respelling, pagename, nil, false, parse_err))
elseif ufind(respelling, "^([a-z]+)/$") or ufind(respelling, "^([a-z]+)/%[[^%[%]]*%]$") then
-- attempt to include a part of speech in all-at-once syntax
parse_err(("Part of speech not allowed when pagename is multiword and all-at-once syntax is used in " ..
"the respelling, but saw '%s'"):format(respelling))
elseif ufind(respelling, "^" .. export.mid_vowel_hint_c .. "$") then
-- attempt to use a mid-vowel hint in all-at-once syntax
parse_err(("Single mid-vowel hint not allowed when pagename is multiword because it's not clear which " ..
"word to apply it to, but saw '%s'"):format(respelling))
elseif ufind(respelling, "[+%[%]]") or ufind(respelling, "^" .. export.mid_vowel_hint_c .. " ") or
ufind(respelling, " " .. export.mid_vowel_hint_c .. " ") or
ufind(respelling, " " .. export.mid_vowel_hint_c .. "$") then
-- word-by-word syntax
local sub_with_space = umatch(respelling, "%[[^%[%]]* [^%[%]]*%]")
if sub_with_space then
parse_err(("When using word-by-word syntax with a multiword pagename, saw substitution spec '%s' " ..
"with spaces, which is not allowed because it must match a single word"):format(sub_with_space))
end
pagename = canon_respelling(pagename)
respelling = canon_respelling(respelling)
local pagename_words = split(pagename, " ")
local respelling_words = split(respelling, " ")
if #pagename_words ~= #respelling_words then
parse_err(("When using word-by-word syntax with a multiword pagename, saw %s words in pagename but " ..
"%s word%s in respelling; they need to match"):format(#pagename_words, #respelling_words,
#respelling_words > 1 and "s" or ""))
end
local word_objs = {}
for i = 1, #pagename_words do
table.insert(word_objs, substitute_respelling_word(respelling_words[i], pagename_words[i]))
end
return {words = word_objs}
else
-- replacement syntax; pagename ignored
return split_respelling_into_words(respelling, "parse pos")
end
else
local word_obj = substitute_respelling_word(respelling, pagename)
word_obj.term = canon_respelling(word_obj.term)
return {words = {word_obj}}
end
end
-- Parse a list of comma-split runs containing one or more respellings, i.e. after calling parse_balanced_segment_run()
-- or the like followed by split_alternating_runs() or the like (see [[Module:parse utilities]]). `pagename` is the
-- pagename, for use when a respelling is just '+', a mid-vowel hint like 'ê' or a substitution spec like '[ks]'.
-- `original_input` is the raw input and `input_param` the name of the param containing the raw input; both are used
-- only in error messages. Return an object specifying the respellings, currently with a single field 'terms' (this
-- format is used in case other outer properties exist in the future), where 'terms' is a list of term objects. Each
-- term object contains either a field `term` with the respelling and an optional part of speech `pos`, or fields
-- `raw_phonemic` and/or `raw_phonetic` (if the user specified raw IPA using "/.../" or "/.../ [...]" or "raw:[...]"),
-- `unknown` (if the user specified "?"), or `omitted` (if the user specified "-"). In addition, there may be fields
-- `q`, `qq`, `a`, `aa`, and/or `ref` corresponding to inline modifiers. Each such field is a list; all are lists of
-- strings except for `ref`, which is a list of objects as returned by parse_references() in [[Module:references]].
function export.parse_comma_separated_groups(comma_separated_groups, pagename, original_input, input_param)
local function generate_obj(respelling, parse_err)
return parse_respelling(respelling, pagename == true and respelling or pagename, parse_err)
end
local put = require(parse_utilities_module)
local outer_container = {terms = {}}
for _, group in ipairs(comma_separated_groups) do
-- Rejoin runs that don't involve <...>.
local j = 2
while j <= #group do
if not group[j]:find("^<.*>$") then
group[j - 1] = group[j - 1] .. group[j] .. group[j + 1]
table.remove(group, j)
table.remove(group, j)
else
j = j + 2
end
end
local param_mods = {
-- pre = { overall = true },
-- post = { overall = true },
ref = { store = "insert", convert = function(arg, parse_err)
return require("Module:references").parse_references(arg)
end },
q = { store = "insert" },
qq = { store = "insert" },
a = { store = "insert" },
aa = { store = "insert" },
}
table.insert(outer_container.terms, put.parse_inline_modifiers_from_segments {
group = group,
arg = original_input,
props = {
paramname = input_param,
param_mods = param_mods,
generate_obj = generate_obj,
splitchar = ",",
outer_container = outer_container,
},
})
end
return outer_container
end
-- Generate the pronunciation of `words` (a list of word objects representing respellings, each of which is an object
-- of the form {term = RESPELLING, pos = PART_OF_SPEECH} in `dialect` ("cen", "bal" or "val").
local function to_IPA(words, dialect)
local pronuns = {}
for _, wordobj in ipairs(words) do
if ufind(wordobj.term, "[áìùÁÌÙ]") then
error(("Invalid accented character in respelling '%s'; use accented à í ú, not the reversed versions"
):format(wordobj.term))
end
end
words = handle_unstressed_words(words)
for _, wordobj in ipairs(words) do
local word = wordobj.term
local pos = wordobj.pos
local suffix_syllables = {}
local orig_word = word
word = ulower(word)
if not pos or pos == "adverb" then
local word_before_ment = umatch(word, "^(.*)(m[eé]nt)$")
if word_before_ment and (pos == "adverb" or not ufind(word_before_ment, "[iï]$") and
ufind(word_before_ment, V .. ".*" .. V)) then
suffix_syllables = {{onset = "m", vowel = "e", coda = "nt", stressed = true}}
pos = "adjective"
word = word_before_ment
end
end
word = word_fixes(word, dialect)
local syllables = split_syllables(word)
syllables = preprocess_word(syllables, suffix_syllables, dialect, pos, orig_word)
-- Combine syllables.
local combined = {}
local has_ment = #suffix_syllables > 0
for i, syll in ipairs(syllables) do
local ac = (i == syllables.stress and not syllables.is_prefix and not has_ment or
has_ment and i == #syllables) and AC or -- primary stress
syllables[i].stressed and GR or -- secondary stress
""
table.insert(combined, syll.onset .. syll.vowel .. ac .. syll.coda)
end
table.insert(pronuns, table.concat(combined, "."))
end
-- Put double ## at utterance boundaries (beginning/end of string) and at foot boundaries (marked with |).
-- Note that if the string without pound signs is 'foo bar baz | bat quux', the final string will be
-- '##foo# #bar# #baz## #|# ##bat# #quux##'.
local text = "##" .. table.concat(pronuns, " ") .. "##"
text = rsub(text, " | ", "# | #")
text = rsub(text, " ", "# #")
return postprocess_general(text, dialect)
end
-- Generate the phonemic and phonetic pronunciations of the respellings in `parsed_respellings`, which is a table whose
-- keys are dialect identifiers (e.g. "cen" for Central Catalan, "val" for Valencian) and whose values are objects of
-- the format returned by parse_comma_separated_groups() (see comment above that function). This destructively modifies
-- `parsed_respellings`, adding fields `phonemic` and `phonetic` containing the generated pronunciations and removing
-- the input fields used to generate those output fields. (FIXME: Currently only phonetic pronunciation is generated.)
function export.generate_phonemic_phonetic(parsed_respellings)
-- Convert each canonicalized respelling to phonemic/phonetic IPA.
for dialect, respelling_spec in pairs(parsed_respellings) do
for _, termobj in ipairs(respelling_spec.terms) do
if termobj.unknown or termobj.omitted then
-- leave alone, will handle later
elseif termobj.raw_phonemic or termobj.raw_phonetic then
termobj.phonemic = termobj.raw_phonemic
termobj.phonetic = termobj.raw_phonetic
-- set to nil so by-value comparisons respect only the resulting phonemic/phonetic and qualifiers
termobj.raw_phonemic = nil
termobj.raw_phonetic = nil
else
termobj.phonetic = to_IPA(termobj.words, dialect)
-- set to nil so by-value comparisons respect only the resulting phonemic/phonetic and qualifiers
termobj.words = nil
end
end
end
end
-- Group pronunciations by dialect, i.e. grouping pronunciations that are identical in every way (including both the
-- pronunciation(s) and any qualifiers and other inline modifiers). `parsed_respellings` contains the output from
-- generate_phonemic_phonetic(), and the return value is a list of grouped pronunciations, where each object in the list
-- contains fields `dialects` (a list of dialects containing the pronunciations) and `pronuns` (a list of
-- pronunciations, where each pronunciation is specified by an object containing fields `phonemic` and `phonetic`, as
-- generated by generate_phonemic_phonetic(), along with any inline modifier fields `q`, `qq`, `a`, `aa` and/or `ref`).
function export.group_pronuns_by_dialect(parsed_respellings)
local grouped_pronuns = {}
for dialect, pronun_spec in pairs(parsed_respellings) do
local saw_omitted = false
for _, termobj in ipairs(pronun_spec.terms) do
if termobj.omitted then
saw_omitted = true
break
end
end
if not saw_omitted then
local saw_existing = false
for _, group in ipairs(grouped_pronuns) do
if deep_equals(group.pronuns, pronun_spec.terms) then
table.insert(group.dialects, dialect)
saw_existing = true
break
end
end
if not saw_existing then
table.insert(grouped_pronuns, {dialects = {dialect}, pronuns = pronun_spec.terms})
end
end
end
return grouped_pronuns
end
-- Format pronunciations grouped by dialect. `grouped_pronuns` contains the output of group_pronuns_by_dialect().
-- This destructively modifies `grouped_pronuns`, adding a field 'formatted' to the first-level values of
-- `grouped_pronuns` containing the formatted pronunciation(s) for a given set of dialects.
function export.format_grouped_pronunciations(grouped_pronuns)
for _, grouped_pronun_spec in pairs(grouped_pronuns) do
local pronunciations = {}
-- Loop through each pronunciation. For each one, add the phonemic and phonetic versions to `pronunciations`,
-- for formatting by [[Module:IPA]] or raw (for use in [[Module:ca-headword]]).
for j, pronun in ipairs(grouped_pronun_spec.pronuns) do
-- Add dialect tags to left accent qualifiers if first one
local as = pronun.a
if j == 1 then
if as then
as = deep_copy(as)
else
as = {}
end
for _, dialect in ipairs(grouped_pronun_spec.dialects) do
table.insert(as, export.dialects_to_names[dialect])
end
end
local first_pronun = #pronunciations + 1
if pronun.unknown then
-- FIXME: This is a massive hack but it works for now.
table.insert(pronunciations, { pron = "", pretext = "''di-alam''" })
else
if not pronun.phonemic and not pronun.phonetic then
error("Internal error: Saw neither phonemic nor phonetic pronunciation")
end
if pronun.phonemic then -- missing if 'raw:[...]' given
local slash_pron = "/" .. pronun.phonemic .. "/"
table.insert(pronunciations, {
pron = slash_pron,
})
end
if pronun.phonetic then -- missing if '/.../' given
local bracket_pron = "[" .. pronun.phonetic .. "]"
table.insert(pronunciations, {
pron = bracket_pron,
})
end
end
local last_pronun = #pronunciations
if pronun.q then
pronunciations[first_pronun].q = pronun.q
end
if as then
pronunciations[first_pronun].a = as
end
if j > 1 then
pronunciations[first_pronun].separator = ", "
end
if pronun.qq then
pronunciations[last_pronun].qq = pronun.qq
end
if pronun.aa then
pronunciations[last_pronun].aa = pronun.aa
end
if pronun.refs then
pronunciations[last_pronun].refs = pronun.refs
end
if first_pronun ~= last_pronun then
pronunciations[last_pronun].separator = " "
end
end
grouped_pronun_spec.formatted = format_IPA_full{
lang = lang,
items = pronunciations,
separator = "",
}
end
end
function export.show(frame)
local params = {
[1] = {},
indent = {},
pagename = {} -- for testing or documentation pages
}
for _, dialect in ipairs(export.dialects) do
params[dialect] = {}
end
for dialect_group, _ in pairs(export.dialect_groups) do
params[dialect_group] = {}
end
local args = require("Module:parameters").process(frame:getParent().args, params)
local pagename = args.pagename or mw.loadData("Module:headword/data").pagename
-- Set inputs
local inputs = {}
-- If 1= specified, do all dialects.
if args[1] then
for _, dialect in ipairs(export.dialects) do
inputs[dialect] = {input = args[1], param = 1}
end
end
-- Then do dialect groups.
for dialect_group, group_dialects in pairs(export.dialect_groups) do
if args[dialect_group] then
for _, dialect in ipairs(group_dialects) do
inputs[dialect] = {input = args[dialect_group], param = dialect_group}
end
end
end
-- Then do individual dialect settings.
for _, dialect in ipairs(export.dialects) do
if args[dialect] then
inputs[dialect] = {input = args[dialect], param = dialect}
end
end
-- If no inputs given, set all dialects based on current pagename.
if not next(inputs) then
for _, dialect in ipairs(export.dialects) do
inputs[dialect] = {input = "+", param = "(pagename)"}
end
end
-- Parse the arguments.
local parsed_respellings = {}
for dialect, inputspec in pairs(inputs) do
local function generate_obj(respelling, parse_err)
return parse_respelling(respelling, pagename, parse_err)
end
if inputspec.input:find("[<%[]") then
local put = require(parse_utilities_module)
-- Parse balanced segment runs involving either [...] (substitution notation) or <...> (inline modifiers).
-- We do this because we don't want commas inside of square or angle brackets to count as respelling
-- delimiters. However, we need to rejoin square-bracketed segments with nearby ones after splitting
-- alternating runs on comma. For example, if we are given
-- "a[x]a<q:learned>,[vol:vôl,ks]<q:nonstandard>", after calling
-- parse_multi_delimiter_balanced_segment_run() we get the following output:
--
-- {"a", "[x]", "a", "<q:learned>", ",", "[vol:vôl,ks]", "", "<q:nonstandard>", ""}
--
-- After calling split_alternating_runs(), we get the following:
--
-- {{"a", "[x]", "a", "<q:learned>", ""}, {"", "[vol:vôl,ks]", "", "<q:nonstandard>", ""}}
--
-- We need to rejoin stuff on either side of the square-bracketed portions.
local segments = put.parse_multi_delimiter_balanced_segment_run(inputspec.input, {{"<", ">"}, {"[", "]"}})
local comma_separated_groups = put.split_alternating_runs_on_comma(segments)
-- Process each value.
local outer_container = export.parse_comma_separated_groups(comma_separated_groups, pagename,
inputspec.input, inputspec.param)
parsed_respellings[dialect] = outer_container
else
local termobjs = {}
local function parse_err(msg)
error(msg .. ": " .. inputspec.param .. "=" .. inputspec.input)
end
for _, term in ipairs(split_on_comma(inputspec.input)) do
table.insert(termobjs, generate_obj(term, parse_err))
end
parsed_respellings[dialect] = {
terms = termobjs,
}
end
end
-- Convert each canonicalized respelling to phonemic/phonetic IPA.
export.generate_phonemic_phonetic(parsed_respellings)
-- Group the results.
local grouped_pronuns = export.group_pronuns_by_dialect(parsed_respellings)
-- Format the results.
export.format_grouped_pronunciations(grouped_pronuns)
-- Concatenate formatted results.
local formatted = {}
for _, grouped_pronun_spec in ipairs(grouped_pronuns) do
table.insert(formatted, grouped_pronun_spec.formatted)
end
local indent = (args.indent or "*") .. " "
local out = table.concat(formatted, "\n" .. indent)
if args.indent then
out = indent .. out
end
return out
end
-- Used by [[Module:ca-IPA/testcases]].
function export.test(pagename, respelling, dialect)
local function parse_err(msg)
error(msg)
end
local parsed = parse_respelling(respelling, pagename, parse_err)
return to_IPA(parsed.words, dialect)
end
return export
ltrdo6zdfol7ebsziepkg5qbzu4ub4e
Padron:ast-pr
10
39003
178537
2026-10-29T00:29:01Z
GinawaSaHapon
3989
Nilikha ang pahina na may '<includeonly>{{#invoke:ast-IPA|show_pr}}</includeonly><noinclude>{{documentation}}</noinclude>'
178537
wikitext
text/x-wiki
<includeonly>{{#invoke:ast-IPA|show_pr}}</includeonly><noinclude>{{documentation}}</noinclude>
83lytnabn3fbhqjh8jkdm2kw46f1lo2
Padron:ast-noun
10
39004
178539
2026-10-29T00:35:33Z
GinawaSaHapon
3989
Nilikha ang pahina na may '{{#invoke:checkparams|warn}}<!-- Validate template parameters -->{{head|ast|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|suffix|noun}}|head={{{head|}}} |g={{#switch:{{{1}}}|l|mf=l|f=b|#default=?}} |g2={{#switch:{{{1}}}|mf=b}} |{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|hulaping gumagawa ng pangngalan[[Kategorya:Asturyano na hulapi na gumagawa ng pangngalan]]}}| |{{#ifeq:{{{2|}}}|-|di-mabibilangKategorya:Asturyano na pangngalang di-mabi...'
178539
wikitext
text/x-wiki
{{#invoke:checkparams|warn}}<!-- Validate template parameters
-->{{head|ast|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|suffix|noun}}|head={{{head|}}}
|g={{#switch:{{{1}}}|l|mf=l|f=b|#default=?}}
|g2={{#switch:{{{1}}}|mf=b}}
|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|hulaping gumagawa ng pangngalan[[Kategorya:Asturyano na hulapi na gumagawa ng pangngalan]]}}|
|{{#ifeq:{{{2|}}}|-|di-mabibilang[[Kategorya:Asturyano na pangngalang di-mabibilang]]}}|
|{{#ifeq:{{{2|}}}|-||maramihan}}|{{{2|{{pagename}}s}}}
|f3accel-form=p
|checkredlinks=1
}}<noinclude>{{documentation}}</noinclude>
fbxpp8ggpgbltgkp56gidkgosne6gup
178540
178539
2026-10-29T00:36:03Z
GinawaSaHapon
3989
178540
wikitext
text/x-wiki
<!-- Validate template parameters
-->{{head|ast|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|suffix|noun}}|head={{{head|}}}
|g={{#switch:{{{1}}}|l|mf=l|f=b|#default=?}}
|g2={{#switch:{{{1}}}|mf=b}}
|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|hulaping gumagawa ng pangngalan[[Kategorya:Asturyano na hulapi na gumagawa ng pangngalan]]}}|
|{{#ifeq:{{{2|}}}|-|di-mabibilang[[Kategorya:Asturyano na pangngalang di-mabibilang]]}}|
|{{#ifeq:{{{2|}}}|-||maramihan}}|{{{2|{{pagename}}s}}}
|f3accel-form=p
|checkredlinks=1
}}<noinclude>{{documentation}}</noinclude>
00rs4xa1yna3g8nl7h38lrhpjlhb9b0
178541
178540
2026-10-29T00:36:28Z
GinawaSaHapon
3989
178541
wikitext
text/x-wiki
<!-- Validate template parameters
-->{{head|ast|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|suffix|noun}}|head={{{head|}}}
|g={{#switch:{{{1}}}|l|mf=l|f=b|#default=?}}
|g2={{#switch:{{{1}}}|mf=b}}
|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|hulaping gumagawa ng pangngalan[[Kategorya:Asturyano na hulapi na gumagawa ng pangngalan]]}}|
|{{#ifeq:{{{2|}}}|-|di-mabibilang[[Kategorya:Asturyano na pangngalang di-mabibilang]]}}|
|{{#ifeq:{{{2|}}}|-||maramihan}}|{{{2|{{pagename}}s}}}
|f3accel-form=p
|checkredlinks=0
}}<noinclude>{{documentation}}</noinclude>
gvkpxhkjqt82upfy2os8np4nk4jl3iq
178542
178541
2026-10-29T00:37:04Z
GinawaSaHapon
3989
178542
wikitext
text/x-wiki
<!-- Validate template parameters
-->{{head|ast|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|suffix|noun}}|head={{{head|}}}
|g={{#switch:{{{1}}}|l|mf=l|f=b|#default=?}}
|g2={{#switch:{{{1}}}|mf=b}}
|{{#if:{{#invoke:string/templates|find|{{pagename}}|^%-}}|hulaping gumagawa ng pangngalan[[Kategorya:Asturyano na hulapi na gumagawa ng pangngalan]]}}|
|{{#ifeq:{{{2|}}}|-|di-mabibilang[[Kategorya:Asturyano na pangngalang di-mabibilang]]}}|
|{{#ifeq:{{{2|}}}|-||maramihan}}|{{{2|{{pagename}}s}}}
|f3accel-form=p
}}<noinclude>{{documentation}}</noinclude>
7209xvxb9cotvomjvrlxr8slp3nqgfg
Padron:R:ast:DALLA
10
39005
178543
2026-10-29T00:39:32Z
GinawaSaHapon
3989
Nilikha ang pahina na may '{{#invoke:quote|call_template | template = cite-book | ast | title = Diccionariu de la llingua asturiana | year = 2000 | edition = 1 | publisher = w:Academy of the Asturian Language<f:ast:Academia de la Llingua Asturiana> | entry = ast:{{{2|{{{1|{{{entry|{{pagename}}}}}}}}}}} | entryurl = https://diccionariu.alladixital.org//index.php?{{#if:{{{id|}}} | cod={{{id|}}} | pallabra={{urlencode:{{{1|{{{entry|{{pagename}}}}}}}}}} }}...'
178543
wikitext
text/x-wiki
{{#invoke:quote|call_template
| template = cite-book
| ast
| title = Diccionariu de la llingua asturiana
| year = 2000
| edition = 1
| publisher = w:Academy of the Asturian Language<f:ast:Academia de la Llingua Asturiana>
| entry = ast:{{{2|{{{1|{{{entry|{{pagename}}}}}}}}}}}
| entryurl = https://diccionariu.alladixital.org//index.php?{{#if:{{{id|}}}
| cod={{{id|}}}
| pallabra={{urlencode:{{{1|{{{entry|{{pagename}}}}}}}}}}
}}
| isbn = 978-84-8168-208-3
| pageparam = page
| allowparams = 1, 2, entry, id
}}<!--
--><noinclude>{{documentation}}</noinclude>
g1m6carnam7zggyexkrnhil18baz6di
Padron:R:ast:DGLA
10
39006
178544
2026-10-29T00:40:27Z
GinawaSaHapon
3989
Nilikha ang pahina na may '{{#invoke:quote|call_template | template = cite-book | ast | worklang = es | year = 2002–2004 | author = [[w:es:Xosé Lluis García Arias|García Arias, Xosé Lluis]] | title = es:Diccionario general de la lengua asturiana | entry = ast:{{{2|{{{1|{{{entry|{{pagename}}}}}}}}}}} | entryurl = https://mas.lne.es/diccionario/{{#if:{{{id|}}}|palabra/{{{id|}}}|buscador?q={{urlencode:{{{1|{{{entry|{{pagename}}}}}}}}}}}} | publisher =...'
178544
wikitext
text/x-wiki
{{#invoke:quote|call_template
| template = cite-book
| ast
| worklang = es
| year = 2002–2004
| author = [[w:es:Xosé Lluis García Arias|García Arias, Xosé Lluis]]
| title = es:Diccionario general de la lengua asturiana
| entry = ast:{{{2|{{{1|{{{entry|{{pagename}}}}}}}}}}}
| entryurl = https://mas.lne.es/diccionario/{{#if:{{{id|}}}|palabra/{{{id|}}}|buscador?q={{urlencode:{{{1|{{{entry|{{pagename}}}}}}}}}}}}
| publisher = es:Editorial Prensa Asturiana
| isbn = 978-84-87730-86-3
| pageparam = page
| allowparams = 1, 2, entry, id
}}<!--
--><noinclude>{{documentation}}</noinclude>
ifd7qs4l4kldauj2lh72gpcwvw3n2pj
llibru
0
39007
178545
2026-10-29T00:41:04Z
GinawaSaHapon
3989
Nilikha ang pahina na may '=={{=ast=}}== ===Etimolohiya=== Mula sa {{inh|ast|la|liber|liber, librum|aklat}}. ===Pagbigkas=== {{ast-pr}} ===Pangngalan=== {{ast-noun|l|llibros}} # [[aklat]] ====Kaugnay na kataga==== {{col|ast|llibreru}} ===Magbasa pa=== * {{R:ast:DALLA}} * {{R:ast:DGLA}}'
178545
wikitext
text/x-wiki
=={{=ast=}}==
===Etimolohiya===
Mula sa {{inh|ast|la|liber|liber, librum|aklat}}.
===Pagbigkas===
{{ast-pr}}
===Pangngalan===
{{ast-noun|l|llibros}}
# [[aklat]]
====Kaugnay na kataga====
{{col|ast|llibreru}}
===Magbasa pa===
* {{R:ast:DALLA}}
* {{R:ast:DGLA}}
imddxwk3op3i8v3tbvdmy9go7vwcm00
Padron:R:ca:DNV
10
39008
178550
2026-10-29T01:29:17Z
GinawaSaHapon
3989
Nilikha ang pahina na may '{{#invoke:checkparams|error}}<!-- Validate template parameters -->[https://www.avl.gva.es/lexicval/xhtml/dnv.xhtml?paraula={{urlencode:{{{1|{{pagename}}}}}}} “{{{2|{{{1|{{pagename}}}}}}}}”] sa ''Diccionari normatiu valencià'', [[w:Acadèmia Valenciana de la Llengua|Acadèmia Valenciana de la Llengua]].<noinclude>{{documentation}}</noinclude>'
178550
wikitext
text/x-wiki
{{#invoke:checkparams|error}}<!-- Validate template parameters
-->[https://www.avl.gva.es/lexicval/xhtml/dnv.xhtml?paraula={{urlencode:{{{1|{{pagename}}}}}}} “{{{2|{{{1|{{pagename}}}}}}}}”] sa ''Diccionari normatiu valencià'', [[w:Acadèmia Valenciana de la Llengua|Acadèmia Valenciana de la Llengua]].<noinclude>{{documentation}}</noinclude>
236i9h5r7m1x524rsids24i3bipwkim
llibre
0
39009
178552
2026-10-29T01:30:34Z
GinawaSaHapon
3989
Nilikha ang pahina na may '=={{=ca=}}== ===Etimolohiya=== {{inh+|ca|roa-oca|libre}}, mula {{der|ca|la|liber|librum|book}}. Dahil sa /i/, posibleng hiram. ===Pagbigkas=== * {{ca-IPA}} * {{audio|ca|LL-Q7026 (cat)-Toniher-llibre.wav|a=Catalonia}} ===Pangngalan=== {{ca-noun|m}} # [[aklat]] ====Kaugnay na kataga==== {{col3|ca |llibrer |llibreria |llibresc |llibret |llibreta |llibreter |llibreteria |llibretista}} ===Magbasa pa=== * {{R:ca:IEC2}} * {{R:ca:GDLC}} * {{R:ca:DNV}} * {{R:ca:DCVB}}'
178552
wikitext
text/x-wiki
=={{=ca=}}==
===Etimolohiya===
{{inh+|ca|roa-oca|libre}}, mula {{der|ca|la|liber|librum|book}}. Dahil sa /i/, posibleng hiram.
===Pagbigkas===
* {{ca-IPA}}
* {{audio|ca|LL-Q7026 (cat)-Toniher-llibre.wav|a=Catalonia}}
===Pangngalan===
{{ca-noun|m}}
# [[aklat]]
====Kaugnay na kataga====
{{col3|ca
|llibrer
|llibreria
|llibresc
|llibret
|llibreta
|llibreter
|llibreteria
|llibretista}}
===Magbasa pa===
* {{R:ca:IEC2}}
* {{R:ca:GDLC}}
* {{R:ca:DNV}}
* {{R:ca:DCVB}}
mj30yjvpbajp3j6d6r2q4pearz7reim
178553
178552
2026-10-29T01:34:14Z
GinawaSaHapon
3989
178553
wikitext
text/x-wiki
=={{=ca=}}==
===Etimolohiya===
{{inh+|ca|roa-oca|libre}}, mula {{der|ca|la|liber|librum|book}}. Dahil sa /i/, posibleng hiram.
===Pagbigkas===
* {{ca-IPA}}
* {{audio|ca|LL-Q7026 (cat)-Toniher-llibre.wav|a=Katalonya}}
===Pangngalan===
{{ca-noun|m}}
# [[aklat]]
====Kaugnay na kataga====
{{col3|ca
|llibrer
|llibreria
|llibresc
|llibret
|llibreta
|llibreter
|llibreteria
|llibretista}}
===Magbasa pa===
* {{R:ca:IEC2}}
* {{R:ca:GDLC}}
* {{R:ca:DNV}}
* {{R:ca:DCVB}}
4mpev95i90e3xt0azutec4iqm39quua