Pergi ke kandungan

Modul:IPA

Daripada Wikikamus


local export = {}

local force_cat = false -- for testing

local decorations_module = "Module:decorations"
local pages_module = "Module:pages"
local qualifier_module = "Module:qualifier"
local string_utilities_module = "Module:string utilities"
local syllables_module = "Module:syllables"
local utilities_module = "Module:utilities"

local m_data = mw.loadData("Module:IPA/data")
local m_str_utils = require(string_utilities_module)
local m_syllables -- [[Module:syllables]]; loaded below if needed
local m_symbols = mw.loadData("Module:IPA/data/symbols")

local concat = table.concat
local decode_entities = m_str_utils.decode_entities
local find = string.find
local gcodepoint = m_str_utils.gcodepoint
local gmatch = m_str_utils.gmatch
local gsub = string.gsub
local insert = table.insert
local is_preview = require(pages_module).is_preview
local len = m_str_utils.len
local listToText = mw.text.listToText
local match = string.match
local pattern_escape = m_str_utils.pattern_escape
local sub = string.sub
local u = m_str_utils.char
local ugsub = m_str_utils.gsub
local umatch = m_str_utils.match
local usub = m_str_utils.sub

local function with_codepoints(s)
	if find(s, "%S%s") then
		local parts = {}
		for ch in gmatch(s, "%S") do
			parts[#parts + 1] = with_codepoints(ch)
		end
		return concat(parts, ", ")
	end
	local cps = {}
	for cp in gcodepoint(s) do
		cps[#cps + 1] = ("U+%04X"):format(cp)
	end
	return s .. " [" .. concat(cps, " ") .. "]"
end

local namespace = mw.title.getCurrentTitle().nsText
local function is_content_page(lang, namespace)
	return namespace == "" or namespace == "Rekonstruksi" or
		lang and lang:hasType("appendix-constructed") and namespace == "Lampiran"
end

-- Etymology-only languages are not L2 entry languages; IPA should use the parent full language.
local function assert_not_etymology_only_lang(lang)
	if lang and lang.hasType and lang:hasType("language", "etymology-only") then
		local parent_code = lang.getParentCode and lang:getParentCode() or nil
		error(("Cannot use IPA with the etymology-only language %q; use the parent full language %q instead."):format(lang:getCode(), parent_code))
	end
end

local function track(page)
	require("Module:debug/track")("IPA/" .. page)
	return true
end

local function process_maybe_split_categories(split_output, categories, prontext, lang, errtext)
	if split_output ~= "raw" then
		if categories[1] then
			categories = require(utilities_module).format_categories(categories, lang, nil, nil, force_cat)
		else
			categories = ""
		end
	end
	if split_output then -- for use of IPA in links, etc.
		if errtext then
			return prontext, categories, errtext
		else
			return prontext, categories
		end
	else
		return prontext .. (errtext or "") .. categories
	end
end

--[==[
Format a line of one or more IPA pronunciations as {{tl|IPA}} would do it, i.e. with a preceding {"IPA:"} followed by
the word {"key"} linking to an Appendix page describing the language's phonology, and with an added category
` ``lang`` terms with IPA pronunciation`. Other than the extra preceding text and category, this is identical
to {format_IPA_multiple()}, and the considerations described there in the documentation apply here as well. There is a
single parameter `data`, an object with the following fields:
* `lang`: Object representing the language of the pronunciations, which is used when adding cleanup categories for
   pronunciations with invalid phonemes; for determining how many syllables the pronunciations have in them, in order to
   add a category such as [[:Category:Italian 2-syllable words]] (for certain languages only); for adding a category
   ` ``lang`` terms with IPA pronunciation`; and for determining the proper sort keys for categories. Unlike
   for {format_IPA_multiple()}, `lang` may not be {nil}.
* `items`: List of pronunciations, in exactly the same format as for {format_IPA_multiple()}.
* `err`: If not {nil}, a string containing an error message to use in place of the link to the language's phonology.
* `separator`: The default separator to use when separating formatted items. Defaults to {", "}. Does not apply to the
  first item, where the default separator is always the empty string. Overridden by the per-item `separator` field in
  `items`.
* `sort_key`: Explicit sort key used for categories.
* `no_count`: Suppress adding a {#-syllable words} category such as [[:Category:Italian 2-syllable words]]. Note that
  only certain languages add such categories to begin with, because it depends on knowing how to count syllables in a
  given language, which depends on the phonology of the language. Also, this does not suppress the addition of cleanup
  or other categories. If you need them suppressed, use `split_output` to return the categories separately and ignore
  them.
* `split_output`: If not given, the return value is a concatenation of the formatted pronunciation and formatted
  categories. Otherwise, two values are returned: the formatted pronunciation and the categories. If `split_output` is
  the value {"raw"}, the categories are returned in list form, where the list elements are a combination of category
  strings and category objects of the form suitable for passing to {format_categories()} in [[Module:utilities]]. If
  `split_output` is any other value besides {nil}, the categories are returned as a pre-formatted concatenated string.
* `include_langname`: If specified, prefix the result with the language name, followed by a colon.
* `q`: {nil} or a list of left qualifiers (as in {{tl|q}}) to display at the beginning, before the formatted
  pronunciations and preceding {"IPA:"}.
* `qq`: {nil} or a list of right qualifiers to display after all formatted pronunciations.
* `a`: {nil} or a list of left accent qualifiers (as in {{tl|a}}) to display at the beginning, before the formatted
  pronunciations and preceding {"IPA:"}.
* `aa`: {nil} or a list of right accent qualifiers to display after all formatted pronunciations.
]==]
function export.format_IPA_full(data)
	if type(data) ~= "table" or data.getCode then
		error("Must now supply a table of arguments to format_IPA_full(); first argument should be that table, not a language object")
	end
	local lang = data.lang
	local items = data.items
	local err = data.err
	local separator = data.separator
	local sort_key = data.sort_key
	local no_count = data.no_count
	local split_output = data.split_output
	local q = data.q
	local qq = data.qq
	local a = data.a
	local aa = data.aa
	local include_langname = data.include_langname
	if data.qualifiers then
		-- FIXME: added 2026-09-18; consider removing eventually.
		error("overall `.qualifiers` is no longer supported; change the code to use `.q` or `.qq`")
	end

	local hasKey = m_data.langs_with_infopages

	if not lang or not lang.getCode then
		error("Must specify language to format_IPA_full()")
	end
	assert_not_etymology_only_lang(lang)
	local langname = lang:getCanonicalName()

	local prefix_text
	if err then
		prefix_text = '<span class="error">' .. err .. '</span>'
	else
		if hasKey[lang:getCode()] then
			prefix_text = "Lampiran:Sebutan bahasa " .. langname
		else
			prefix_text = "wikipedia:Fonologi bahasa " .. langname
		end
		prefix_text = "[[" .. prefix_text .. "|kekunci]]"
	end

	local prefix = "[[Wikikamus:Abjad Fonetik Antarabangsa|AFA]]<sup>(" .. prefix_text .. ")</sup>:&#32;"

	local IPAs, categories = export.format_IPA_multiple(lang, items, separator, no_count, "raw")

	if is_content_page(lang, namespace) then
		insert(categories, {
			cat = "Perkataan dengan sebutan AFA bahasa " .. langname,
			sort_key = sort_key
		})
	end

	local prontext = prefix .. IPAs
	if q and q[1] or qq and qq[1] or a and a[1] or aa and aa[1] then
		prontext = require(decorations_module).format_decorations {
			lang = lang,
			text = prontext,
			q = q,
			qq = qq,
			a = a,
			aa = aa,
		}
	end
	if include_langname then
		prontext = langname .. ": " .. prontext
	end
	return process_maybe_split_categories(split_output, categories, prontext, lang)
end

local function split_phonemic_phonetic(pron)
	local reconstructed, phonemic, phonetic = match(pron, "^(%*?)(/.-/)%s+(%[.-%])$")
	if reconstructed then
		return reconstructed .. phonemic, reconstructed .. phonetic
	else
		return pron, nil
	end
end

local function determine_repr(pron)
	local reconstructed

	-- Temporarily remove any initial asterisk before representation marks,
	-- which avoids having to account for it in the data, but set the
	-- `reconstructed` flag.
	if sub(pron, 1, 1) == "*" then
		reconstructed = true
		pron = sub(pron, 2)
	end

	-- Some representation types have aliases for convenience (e.g. "// //" is
	-- an alias for "⫽ ⫽"). and these need to be substituted in before checking
	-- for other data.
	local opening, n = match(pron, "^.[\128-\191]*")
	local subs_data = m_data.representation_subs[opening]
	if subs_data then
		pron, n = ugsub(pron, subs_data[1], subs_data[2])
		-- If the substitution was made, `opening` needs to be changed to the
		-- new opening character.
		if n ~= 0 then
			opening = subs_data[3]
		end
	end

	-- Get the type data based on the opening character (if any), and set the
	-- representation type if the closing character matches.
	local type_data, repr, closing = m_data.representation_types[opening]
	if type_data then
		closing = type_data[2]
		if type_data and match(pron, pattern_escape(closing) .. "$", #opening + 1) then
			repr = type_data[1]
		end
	end

	-- Default to the empty string.
	if not repr then
		opening, closing = "", ""
	end

	-- Reattach the asterisk if reconstructed.
	if reconstructed then
		pron = "*" .. pron
	end

	return pron, repr, opening, closing, reconstructed
end

local function hasInvalidSeparators(transcription)
	-- Escape certain characters as well as pauses, which have the format "(...)" (with any number of dots), to avoid false-positives.
	transcription = transcription:gsub(".[\128-\191]*", m_symbols.separator_escapes)
		:gsub("%(%.+%)", "\3")
		:gsub("[()]+", "")
	return (
		transcription:find("..", nil, true) or
		transcription:match("%.%f[%z \1\2\3,:;]") or
		transcription:match("\1%f[%z \2\3,:;]") or
		transcription:match("\2%f[%z \1\3,:;]") or
		transcription:match("\3[:;]") or
		transcription:match("%f[^%z \1\2\3,]%.")
	) and true or false
end

--[==[
Format a line of one or more bare IPA pronunciations (i.e. without any preceding {"IPA:"} and without adding to a
category ` ``lang`` terms with IPA pronunciation`). Individual pronunciations are formatted using
{format_IPA()} and are combined with separators, decorations, pre-text, post-text, etc. to form a line of pronunciations.
Parameters accepted are:
* `lang` is an object representing the language of the pronunciations, which is used when adding cleanup categories for
   pronunciations with invalid phonemes; for determining how many syllables the pronunciations have in them, in order to
   add a category such as [[:Category:Italian 2-syllable words]] (for certain languages only); and for computing the
   proper sort keys for categories. `lang` may be {nil}.
* `items` is a list of pronunciations, each of which is an object with the following properties:
** `pron`: the pronunciation, in the same format as is accepted by {format_IPA()}, i.e. it should be either phonemic
     (surrounded by {/.../}), phonetic (surrounded by {[...]}), orthographic (surrounded by {⟨...⟩}) or a rhyme
	 (beginning with a hyphen);
** `pretext`: text to display directly before the formatted pronunciation, inside of any qualifiers or accent
     qualifiers;
** `posttext`: text to display directly after the formatted pronunciation, inside of any qualifiers or accent
     qualifiers;
** `q`: {nil} or a list of left qualifiers (as in {{tl|q}}) to display before the formatted pronunciation;
** `qq`: {nil} or a list of right qualifiers to display after the formatted pronunciation;
** `a`: {nil} or a list of left accent qualifiers (as in {{tl|a}}) to display before the formatted pronunciation;
** `aa`: {nil} or a list of right accent qualifiers to after before the formatted pronunciation;
** `refs`: {nil} or a list of references or reference specs to add after the pronunciation and any posttext and
     qualifiers; 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;
** `gloss`: {nil} or a gloss (definition) for this item, if different definitions have different pronunciations;
** `pos`: {nil} or a part of speech for this item, if different parts of speech have different pronunciations;
** `separator`: the separator text to insert directly before the formatted pronunciation and all decorations
   and pre-text; defaults to the outer `separator` parameter.
* `separator`: The default separator to use when separating formatted items. Defaults to {", "}. Does not apply to the
  first item, where the default separator is always the empty string. Overridden by the per-item `separator` field in
  `items`.
* `no_count`: Suppress adding a {#-syllable words} category such as [[:Category:Italian 2-syllable words]]. Note that
  only certain languages add such categories to begin with, because it depends on knowing how to count syllables in a
  given language, which depends on the phonology of the language. Also, this does not suppress the addition of cleanup
  categories. If you need them suppressed, use `split_output` to return the categories separately and ignore them.
* `split_output`: If not given, the return value is a concatenation of the formatted pronunciation and formatted
  categories. Otherwise, two values are returned: the formatted pronunciation and the categories. If `split_output` is
  the value {"raw"}, the categories are returned in list form, where the list elements are a combination of category
  strings and category objects of the form suitable for passing to {format_categories()} in [[Module:utilities]]. If
  `split_output` is any other value besides {nil}, the categories are returned as a pre-formatted concatenated string.
]==]
function export.format_IPA_multiple(lang, items, separator, no_count, split_output)
	local categories = {}
	separator = separator or ", "

	if not lang then
		track("format-multiple-nolang")
	else
		assert_not_etymology_only_lang(lang)
	end

	-- Format
	if not items[1] then
		if namespace == "Templat" then
			insert(items, {pron = "/aɪ piː ˈeɪ/"})
		else
			insert(categories, "Templat sebutan tanpa sebutan")
		end
	end

	local bits = {}

	for i, item in ipairs(items) do
		local bit

		-- If the pronunciation is entirely empty, allow this and don't do anything, so that e.g. the pretext and/or
		-- posttext can be specified to force something like ''unknown'' to appear in place of the pronunciation
		-- (as happens e.g. when ? is used as a respelling in [[Module:ca-IPA]]; see [[guèiser]] for an example).
		if item.pron == "" then
			bit = ""
		else
			local item_categories, errtext
			bit, item_categories, errtext = export.format_IPA(lang, item.pron, "raw")
			bit = bit .. errtext
			for _, cat in ipairs(item_categories) do
				insert(categories, cat)
			end
		end

		if item.pretext then
			bit = item.pretext .. bit
		end

		if item.posttext then
			bit = bit .. item.posttext
		end

		if item.qualifiers then
			-- FIXME: added 2026-09-18; consider removing eventually.
			error("`.qualifiers` is no longer supported; change the code to use `.q` or `.qq`")
		end
		local has_decorations = item.q and item.q[1] or item.qq and item.qq[1] or item.a and item.a[1] or
			item.aa and item.aa[1] or item.refs and item.refs[1]
		local has_gloss_or_pos = item.gloss or item.pos
		if has_decorations or has_gloss_or_pos then
			-- FIXME: Currently we tack the gloss and POS (in that order) onto the end of the regular left qualifiers.
			-- Should we do something different?
			local q = item.q
			if has_gloss_or_pos then
				q = mw.clone(item.q) or {}
				if item.gloss then
					local m_qualifier = require(qualifier_module)
					insert(q, m_qualifier.wrap_qualifier_css("“", "quote") .. item.gloss ..
						m_qualifier.wrap_qualifier_css("”", "quote"))
				end
				if item.pos then
					-- FIXME: Consider expanding aliases as found in [[Module:headword/data]] or similar.
					insert(q, item.pos)
				end
			end

			bit = require(decorations_module).format_decorations {
				lang = lang,
				text = bit,
				q = q,
				qq = item.qq,
				a = item.a,
				aa = item.aa,
				refs = item.refs,
			}
		end

		bit = (item.separator or (i == 1 and "" or separator)) .. bit

		insert(bits, bit)

		--[=[	[[Special:WhatLinksHere/Wiktionary:Tracking/IPA/syntax-error]]
				The length or gemination symbol should not appear after a syllable break or stress symbol.	]=]

		-- The nature of the following pattern match is such that we don't have to split a combined '/.../ [...]' spec
		-- into its parts in order to process.
		if match(item.pron, "[.\203][\136\140]?\203[\144\145]") then -- [.ˈˌ][ːˑ]
			track("syntax-error")
		end

		if lang then
			-- Add syllable count if the language's diphthongs are listed in [[Module:syllables]].
			-- Don't do this if the term has spaces, a liaison mark (‿) or isn't in mainspace.
			if not no_count and namespace == "" then
				m_syllables = m_syllables or require(syllables_module)
				local langcode = lang:getCode()
				if m_data.langs_to_generate_syllable_count_categories[langcode] then
					local raw_phonemic, phonetic, use_it = split_phonemic_phonetic(item.pron)
					local phonemic, repr = determine_repr(raw_phonemic)
					if not phonetic then -- not a '/.../ [...]' combined pronunciation
						if m_data.langs_to_use_phonetic_or_phonemic_notation[langcode] then
							use_it = phonemic
						elseif m_data.langs_to_use_phonetic_notation[langcode] then
							use_it = repr == "phonetic" and phonemic or nil
						else
							use_it = repr == "phonemic" and phonemic or nil
						end
					elseif repr == "phonetic" then
						use_it = phonetic
					elseif repr == "phonemic" then
						use_it = phonemic
					end
					-- Note: two uses of find with plain patterns is much faster than umatch with [ ‿].
					if use_it and not (find(use_it, " ") or find(use_it, "‿")) then
						local syllable_count = m_syllables.getVowels(use_it, lang)
						if syllable_count then
							insert(categories, "Perkataan " .. syllable_count .. " suku kata bahasa " .. lang:getCanonicalName())
						end
					end
				end
			end
		end
	end

	return process_maybe_split_categories(split_output, categories, concat(bits), lang)
end

--[=[
Format a single IPA pronunciation, which cannot be a combined spec (such as {/.../ [...]}). This has been extracted from
{format_IPA()} to allow the latter to handle such combined specs. This works like {format_IPA()} but requires that
pre-created {err} (for error messages) and {categories} lists be passed in, and adds any generated error messages and
categories to those lists. A single value is returned, the pronunciation, which is usually the same as passed in, but
may have HTML added surrounding invalid characters so they appear in red.
]=]
local function format_one_IPA(lang, raw_pron, err, categories)
	-- Disallow wikilinks.
	if match(raw_pron, "%[%[.-%]%]") then
		error("IPA input must not contain wikilinks.")
	end
	
	raw_pron = decode_entities(raw_pron)

	-- Detect the type of transcription.
	local pron, repr, opening, closing, reconstructed = determine_repr(raw_pron)
	
	-- Strip any reconstruction asterisk and representation marks.
	pron = sub(pron, #opening + 1 + (reconstructed and 1 or 0), -#closing - 1)
	
	if not repr then
		insert(categories, "Sebutan AFA dengan tanda perwakilan tidak sah")
		-- insert(err, "tanda perwakilan tidak sah")
		-- Removed because it's annoying when previewing pronunciation pages.
	end
	if repr ~= "orthographic" and lang and lang:getCode() == "en" and hasInvalidSeparators(pron) then
		insert(categories, "Sebutan AFA bahasa Inggeris dengan pemisah tidak sah")
	end

	if pron == "" then
		insert(categories, "Sebutan AFA dengan tiada sebutan")
	end

	-- Check for obsolete and nonstandard symbols
	for _, symbol in ipairs(m_data.nonstandard) do
		local result
		for nonstandard in gmatch(pron, symbol) do
			if not result then
				result = {}
			end
			insert(result, nonstandard)
			insert(categories,
				{cat = "Sebutan AFA dengan aksara usang atau tidak standard", sort_key = nonstandard}
			)
		end

		if result then
			insert(err, "aksara usang atau tidak standard (" .. concat(result) .. ")")
			break
		end
	end

	--[[ Check for invalid symbols after removing the following:
			1. wikilinks (handled above)
			2. paired HTML tags
			3. bolding
			4. italics
			5. asterisk at beginning of transcription
			6. comma followed by spacing characters
			7. superscripts enclosed in superscript parentheses		]]
	local found_HTML
	local result = gsub(pron, "<(%a+)[^>]*>([^<]+)</%1>",
		function(tagName, content)
			found_HTML = true
			return content
		end)
	result = gsub(result, "'''([^']*)'''", "%1")
	result = gsub(result, "''([^']*)''", "%1")
	result = gsub(result, "^%*", "")
	result = ugsub(result, ",%s+", "")

	-- VS15
	local vs15_class = "[" .. m_symbols.add_vs15 .. "]"
	if umatch(pron, vs15_class) then
		local vs15 = u(0xFE0E)
		if find(result, vs15) then
			result = gsub(result, vs15, "")
			pron = gsub(pron, vs15, "")
		end
		pron = ugsub(pron, vs15_class, "%0" .. vs15)
	end

	if result ~= "" then
		local content_page = is_content_page(lang, namespace)
		if lang then
			-- Get the per_lang_valid data, and convert any per-language valid sequences to spaces.
			local per_lang_valid = m_symbols.per_lang_valid[lang:getCode()]
			if per_lang_valid then
				if type(per_lang_valid) == "table" then
					for _, pattern in pairs(per_lang_valid) do
						result = ugsub(result, pattern, " ")
					end
				else -- Should be a string.
					result = ugsub(result, per_lang_valid, " ")
				end
			end
		end
		local suggestions = {}
		-- Check for any invalid sequences, excluding anything in the per-language lookup table.
		for k, v in pairs(m_symbols.invalid) do
			if find(result, k, nil, true) then
				insert(suggestions, with_codepoints(k) .. " dengan " .. with_codepoints(v))
			end
		end
		if suggestions[1] then
			local replacements = "menggantikan " .. listToText(suggestions)
			if content_page then
				error("Invalid IPA: " .. replacements)
			end
			insert(err, replacements)
		end
		-- Convert any valid character sequences to spaces
		for _, pattern in pairs(m_symbols.valid) do
			result = ugsub(result, pattern, " ")
		end
		if not match(result, "^ *$") then
			local category = "Sebutan AFA dengan aksara AFA tidak sah"
			if not content_page then
				category = category .. "/non_mainspace"
			end
			insert(categories, category)
			insert(err, "aksara AFA tidak sah: " .. with_codepoints(result))
		end
	end

	if found_HTML then
		insert(categories, "Sebutan AFA dengan tag HTML berpasangan")
	end

	if (repr == "phonemic" or repr == "rhyme") and lang and m_data.phonemes[lang:getCode()] then
		local valid_phonemes = m_data.phonemes[lang:getCode()]
		local rest = pron
		local phonemes = {}

		while #rest > 0 do
			local longestmatch, longestmatch_len = "", 0

			local rest_init = sub(rest, 1, 1)
			if rest_init == "(" or rest_init == ")" then
				longestmatch = rest_init
				longestmatch_len = 1
			else
				for _, phoneme in ipairs(valid_phonemes) do
					local phoneme_len = len(phoneme)
					if phoneme_len > longestmatch_len and usub(rest, 1, phoneme_len) == phoneme then
						longestmatch = phoneme
						longestmatch_len = len(longestmatch)
					end
				end
			end

			if longestmatch_len > 0 then
				insert(phonemes, longestmatch)
				rest = usub(rest, longestmatch_len + 1)
			else
				local phoneme = usub(rest, 1, 1)
				insert(phonemes, "<span style=\"color: var(--wikt-palette-red,red)\">" .. phoneme .. "</span>")
				rest = usub(rest, 2)
				insert(categories, "Sebutan AFA dengan fonem tidak sah/" .. lang:getCode())
				track("fonem tidak sah/" .. phoneme)
			end
		end

		pron = concat(phonemes)
	end

	return (reconstructed and "*" or "") .. opening .. pron .. closing
end

--[==[
Format an IPA pronunciation. This wraps the pronunciation in appropriate CSS classes and adds cleanup categories and
error messages as needed. The pronunciation `pron` should be either phonemic (surrounded by {/.../}), phonetic
(surrounded by {[...]}), orthographic (surrounded by {⟨...⟩}), a rhyme (beginning with a hyphen) or a combined
phonemic/phonetic spec (of the form {/.../ [...]}). `lang` indicates the language of the pronunciation and can be {nil}.
If not {nil}, and the specified language has data in [[Module:IPA/data]] indicating the allowed phonemes, then the page
will be added to a cleanup category and an error message displayed next to the outputted pronunciation. Note that {lang}
also determines sort key processing in the added cleanup categories. If `split_output` is not given, the return value is
a concatenation of the formatted pronunciation, error messages and formatted cleanup categories. Otherwise, three values
are returned: the formatted pronunciation, the cleanup categories and the concatenated error messages. If `split_output`
is the value {"raw"}, the cleanup categories are returned in list form, where the list elements are a combination of
category strings and category objects of the form suitable for passing to {format_categories()} in [[Module:utilities]].
If `split_output` is any other value besides {nil}, the cleanup categories are returned as a pre-formatted concatenated
string.
]==]
function export.format_IPA(lang, pron, split_output)
	local err = {}
	local categories = {}

	-- `pron` shouldn't contain ref tags.
	if match(pron, "\127'\"`UNIQ%-%-ref%-[%dA-F]+%-QINU`\"'\127") then
		error("<ref> tags found inside pronunciation parameter.")
	end

	if not lang then
		track("format-nolang")
	else
		assert_not_etymology_only_lang(lang)
	end

	local phonemic, phonetic = split_phonemic_phonetic(pron)
	pron = format_one_IPA(lang, phonemic, err, categories)
	if phonetic then
		track("phonemic-phonetic") -- There's no benefit to supporting the "/.../ [...]" format within one parameter.
		phonetic = format_one_IPA(lang, phonetic, err, categories)
		pron = pron .. " " .. phonetic
	end

	if err[1] and is_preview() then
		err = '<span class="error" style="font-size: small;>&#32;' .. concat(err, ", ") .. "</span>"
	else
		err = ""
	end

	return process_maybe_split_categories(split_output, categories, '<span class="IPA nowrap">' .. pron .. "</span>", lang,
		err)
end

--[==[
Format a line of one or more enPR pronunciations as {{tl|enPR}} would do it, i.e. with a preceding {"enPR:"} (linked to
[[Appendix:English pronunciation]]) followed by one or more formatted, comma-separated enPR pronunciations. The
pronunciations are formatted by wrapping them in the `AHD` and `enPR` CSS classes and adding any decorations
(qualifiers, accent qualifiers and references). In addition, the overall result is wrapped in any overall decorations.
There is a single parameter `data`, an object with the following fields:
* `items` is a list of enPR pronunciations, each of which is an object with the following properties:
** `pron`: the enPR pronunciation;
** `q`: {nil} or a list of left qualifiers (as in {{tl|q}}) to display before the formatted pronunciation;
** `qq`: {nil} or a list of right qualifiers to display after the formatted pronunciation;
** `a`: {nil} or a list of left accent qualifiers (as in {{tl|a}}) to display before the formatted pronunciation;
** `aa`: {nil} or a list of right accent qualifiers to after before the formatted pronunciation.
* `q`: {nil} or a list of left qualifiers (as in {{tl|q}}) to display at the beginning, before the formatted
  pronunciations and preceding {"enPR:"}.
* `qq`: {nil} or a list of right qualifiers to display after all formatted pronunciations.
* `a`: {nil} or a list of left accent qualifiers (as in {{tl|a}}) to display at the beginning, before the formatted
  pronunciations and preceding {"enPR:"}.
* `aa`: {nil} or a list of right accent qualifiers to display after all formatted pronunciations.
]==]
function export.format_enPR_full(data)
	local prefix = "[[Appendix:English pronunciation|enPR]]: "
	local lang = require("Module:languages").getByCode("en")
	local parts = {}

	for _, item in ipairs(data.items) do
		local part = '<span class="AHD enPR">' .. item.pron .. "</span>"

		if item.qualifiers then
			-- FIXME: added 2026-09-18; consider removing eventually.
			error("`.qualifiers` is no longer supported; change the code to use `.q` or `.qq`")
		end
		if item.q and item.q[1] or item.qq and item.qq[1] or item.a and item.a[1] or item.aa and item.aa[1] then
			part = require(decorations_module).format_decorations {
				lang = lang,
				text = part,
				q = item.q,
				qq = item.qq,
				a = item.a,
				aa = item.aa,
			}
		end
		insert(parts, part)
	end

	local prontext = prefix .. concat(parts, ", ")
	if data.qualifiers then
		-- FIXME: added 2026-09-18; consider removing eventually.
		error("overall `.qualifiers` is no longer supported; change the code to use `.q` or `.qq`")
	end
	if data.q and data.q[1] or data.qq and data.qq[1] or data.a and data.a[1] or data.aa and data.aa[1] then
		prontext = require(decorations_module).format_decorations {
			lang = lang,
			text = prontext,
			q = data.q,
			qq = data.qq,
			a = data.a,
			aa = data.aa,
		}
	end

	return prontext
end

return export