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