Module:dialect synonyms

See {{dialect synonyms}}.


--[==[
mod: intitle:dial -intitle:data -intitle:documentation -intitle:zh
Module:zh-Christian-syn
]==]

local export = {}
local m_links = require("Module:links")
local m_languages = require("Module:languages")
local m_scripts = require("Module:scripts")
local m_table = require("Module:table")

local langs = {}

local elements = {}

elements.table = function(data)
	return mw.html.create("table")
		:addClass("dial-syn")
		:addClass("wikitable")
		:addClass("mw-collapsible")
		:addClass("mw-collapsed")
	:done()
end

elements.head_a = function(data)
	return mw.html.create("tr")
		:tag("th")
			:attr("colspan", #data.columns)
			:css("background-color", data.colour)
			:wikitext(data.title)
		:done()
	:done()
end

elements.head_b = function(data)
	local tr = mw.html.create("tr")
	:done()

	for i, _ in ipairs(data.columns) do
		tr
			:tag("th")
				:css("background-color", data.colour)
				:wikitext(data.columns[i])
			:done()
		:done()
	end

	return tr
end

elements.row_view_map = function(data)
	local tr = mw.html.create("tr")
	:done()

	tr
		:tag("td")
			:attr("colspan", #data.columns)
			:css("text-align", "right")
			:css("background-color", data.colour)
			:wikitext(("[[%s|view map]]; [[%s|edit data]]"):format(data.view_map, data.edit_link))
		:done()
	:done()

	return tr
end

local function prompt_create_data(dpath, preload_path)
	local url = mw.uri.fullUrl(dpath, {action='edit', preload=preload_path})
	return ("→Create [%s %s]?"):format(tostring(url), dpath)
end

function export.format_word(data_variety, data)
	-- XXX: $ety_n assumes that $language is the first section on a page
	-- and that we can safely say "foo_2" and link to [[foo#Etymology_2]].
	-- this is a fucking stupid assumption,
	-- and we should be using [[T:senseid]].
	--[==[
		data = {"Word", ety_n=5, tr="tr", q="qual"}
		data = "Word_5/tr:qual"
	]==]

	if type(data) == 'string' then
		data = { word = data }
		local temp_a, temp_b = mw.ustring.match(data.word, '^(.+):(.+)$')
		if temp_a then data.word, data.q = temp_a, temp_b end
		local temp_a, temp_b = mw.ustring.match(data.word, '^(.+)/(.+)$')
		if temp_a then data.word, data.tr = temp_a, temp_b end
		local temp_a, temp_b = mw.ustring.match(data.word, '^(.+)_(%d+)$')
		if temp_a then data.word, data.ety_n = temp_a, temp_b end
	end

	-- re-use language objects if possible
	-- $langs is a global
	if not langs[data_variety.code] then
		langs[data_variety.code] = m_languages.getByCode(data_variety.code, nil, true)
	end

	data.lang = langs[data_variety.code]
	data.term, data.alt = data.word, data.word
	if data.ety_n then data.term = ('%s#Etymology %s'):format(data.term, data.ety_n) end
	if data_variety.nolink then data.term = nil end
	data.alt = data.alt .. (data.ety_n and ('<sub>%s</sub>'):format(data.ety_n) or '')

	-- XXX: this code probably shouldn't be located in this module, or at least wholly in this function
	if (data_variety.code_main == 'ar') then
		-- verify script validity
		local valid_script = nil
		local best_script = m_scripts.findBestScriptWithoutLang(data.term):getCode()
		for _,sc in ipairs(data_variety.scripts) do
			if best_script == sc then
				valid_script = sc
				break
			end
		end
		assert(valid_script, ("Invalid script for term `%s` of the dialect `%s`"):format(data.term,data_variety.name))
		data.sc = m_scripts.getByCode(valid_script)

	-- [[Module:ko-dial-syn]]
	-- [[Template:ko-dial-syn#To-do]]
	elseif (data_variety.code_main == 'ko') then
		-- add IPA font for 계림유사
		if (data_variety.name == 'Old Korean-JLLS') and (data.tr) then
			data.tr = tostring(mw.html.create('span')
				:wikitext(data.tr)
				:addClass('IPA')
			:done())
		end

		local bool_no_auto_translit = (
			({['oko']=1, ['okm']=1, ['jje']=1})[data_variety.code]
		)
		local bool_ae_e_merger = (
			(data_variety.name == 'Seoul') or
			(data_variety.code == 'ko-se' and not ({['Jinju']=1, ['Sancheong']=1, ['Hamyang']=1, ['Namhae']=1, ['Hadong']=1, ['Sacheon']=1, ['Goseong']=1, ['Tongyeong']=1})[data_variety.name]) or
			(data_variety.name == 'Samcheok') or
			(data_variety.name == 'Gochang') or
			(data_variety.parent.name == 'South Jeolla' and not ({['Gurye']=1, ['Gwangyang']=1, ['Goheung']=1, ['Boseong']=1})[data_variety.name])
		)
		local bool_eo_eu_merger = (
			(data_variety.code == 'ko-se' and not ({['Uljin']=1, ['Bonghwa']=1, ['Hamyang']=1})[data_variety.name])
		)
		local bool_nasal = (
			(data_variety.code == 'ko-se') or
			(data_variety.parent.name == 'Yeongdong') or
			(data_variety.parent.name == 'South Hwanghae') or
			(data_variety.parent.name == 'North Hwanghae') or
			(data_variety.code == 'ko-nw') or
			(data_variety.code == 'ko-ne')
		)
		local bool_pitch = (
			({['ko-ne']=1, ['ko-se']=1, ['ko-yuk']=1})[data_variety.code] or
			({['Donghae']=1, ['Samcheok']=1, ['Taebaek']=1, ['Yeongwol']=1, ['Gangneung']=1})[data_variety.name]
		)
		local bool_length = true

		if (not data.tr) and (not bool_no_auto_translit) and (bool_ae_e_merger or bool_eo_eu_merger or bool_pitch or bool_length) then
			local m_ko_translit = require("Module:ko-translit")
			local term_for_translit = data.term

			-- transform text in preparation for transliteration
			if (bool_ae_e_merger) then
				term_for_translit = mw.ustring.toNFC(
					mw.ustring.gsub(mw.ustring.toNFD(term_for_translit), "[ᅢᅫ]", {["ᅢ"]="ᅦ", ["ᅫ"]="ᅰ"})
				)
			end
			if (bool_eo_eu_merger) then
				term_for_translit = mw.ustring.toNFC(
					mw.ustring.gsub(mw.ustring.toNFD(term_for_translit), "ᅥ", "ᅳ")
				)
			end
			if (bool_nasal) then
				data.term = mw.ustring.toNFC(
					mw.ustring.gsub(mw.ustring.toNFD(data.term), "~", "ᆼ")
				)
				data.alt = mw.ustring.toNFC(
					mw.ustring.gsub(mw.ustring.toNFD(data.alt), "~", "ᆼ")
				)
			end
			if (bool_pitch) then
				data.term = mw.ustring.gsub(data.term, "'", "")
				data.alt = mw.ustring.gsub(data.alt, "'", "")

				-- assume pitch accent
				if not string.find(term_for_translit, "'") then
					-- gyeongsang
					if data_variety.code == 'ko-se' then
						term_for_translit = mw.ustring.gsub(term_for_translit, "([^~@][~@]*)([^~@][~@]*)$", "'%1%2") -- penultimate
					-- hamgyong
					elseif data_variety.code == 'ko-ne' then
						term_for_translit = mw.ustring.gsub(term_for_translit, "([^~@][~@]*)$", "'%1") -- final
					end
				end

				if (bool_length) then
					term_for_translit = mw.ustring.gsub(term_for_translit, "'@", "@'") -- fix a weird markup order, just in case
				end
				term_for_translit = mw.ustring.gsub(term_for_translit, "('?)([^~@][~@]*)", function(bool_is_high, syllable)
					return syllable .. (bool_is_high ~= "" and "↑" or "↓")
				end)
			end
			if (bool_length) then
				-- XXX: ":" is currently used for word notes; this code tentatively uses "@" instead for development
				data.term = mw.ustring.gsub(data.term, "@", "")
				data.alt = mw.ustring.gsub(data.alt, "@", "")
			end

			-- transliterate
			data.tr = m_ko_translit.tr(term_for_translit)
			-- order diacritics for aesthetics
			data.tr = mw.ustring.gsub(data.tr, "([@↓↑~]+)", function(d)
				return ""
					.. (mw.ustring.match(d, "~") or "")
					.. (mw.ustring.match(d, "[↓↑]") or "")
					.. (mw.ustring.match(d, "@") or "")
			end)
			-- digraph vowels; closed syllables; other modified syllables
			--                                   1 v1     2 v2      3 c                       4         5
			data.tr = mw.ustring.gsub(data.tr, "([aeiou])([aeiou]?)([bcdfghjklmnpqrstvwxyz]*)([↓↑~]+)(@?)", "%1%4%2%5%3")
			-- transform
			data.tr = mw.ustring.gsub(data.tr, "[@↓↑~]", {["@"]=":", ["↑"]="́", ["↓"]="̀", ["~"]="̃"})
		end
	end

	-- create full link
	-- place annotation after the word due to
	-- notes such as those found at [[舅父]],
	-- which appear to rely on direction
	word = m_links.full_link(data)
	if data.q then
		word = word .. " " .. require("Module:qualifier").format_qualifier({ data.q })
	end

	return word
end

local function format_syns(data_variety)
	local words_formatted = {}
	for i, text in ipairs(data_variety.syns) do
		table.insert(words_formatted, export.format_word(data_variety, text))
	end
	return words_formatted
end

function export.show(frame)
	local params = {
		[1] = { required = true, default = "und" },
		[2] = { default = mw.title.getCurrentTitle().text },
		['dpath syns'] = {}, -- for testing: use arbitrary module. like [[Module:sandbox]] or something
	}
	local args = require("Module:parameters").process(frame:getParent().args, params, nil, "dialect synonyms", "show")

	-- data modules
	local dpath = "Module:dialect synonyms"
	local dpath_mul = dpath .. "/" .. "mul"
	local dpath_lang = dpath .. "/" .. args[1]
	local dpath_syns = dpath_lang  .. "/" .. args[2]
	if args['dpath syns'] then dpath_syns = args['dpath syns'] end
	local dpath_map = "Template:dialect synonym map/" .. args[1] .. "/" .. args[2]
	local data_mul = require(dpath_mul)
	local data_lang = mw.title.new(dpath_lang).exists and require(dpath_lang) or nil
	local data_syns = mw.title.new(dpath_syns).exists and require(dpath_syns) or nil

	-- prompt creation of missing data modules
	if (not data_lang) then
		return prompt_create_data(dpath_lang, 'Module:dialect synonyms/und')
	end
	if (not data_syns) then
		return prompt_create_data(dpath_syns, 'Module:dialect synonyms/' .. args[1] .. '/')
	end

	-- throw error if synonyms table does not even exist
	if (not data_syns.syns) and (not data_syns.list) then
		error(('Could not find .syns in [[%s]].'):format(dpath_syns))
	end

	-- create $data_lang.title fallback
	if not data_lang.title then
		data_lang.title = "Dialectal synonyms of %s"
	end
	-- create $data_lang.columns fallback
	if not data_lang.columns then
		data_lang.columns = {"Variety", "Location", "Words"}
	end
	-- create $data_lang.notes fallback
	if not data_lang.notes then
		data_lang.notes = {}
	end

	-- TEMP: backwards compatibility with the original [[Module:zh-dial-syn]] format
	if (not data_syns.syns) and (data_syns.list) then
		data_syns.syns = data_syns.list
		data_syns.list = nil
	end
	if (not data_syns.gloss) and (data_syns.syns.meaning) then
		data_syns.gloss = data_syns.syns.meaning
		data_syns.syns.meaning = nil
	end
	if (not data_syns.note) and (data_syns.syns.note) then
		data_syns.note = data_syns.syns.note
		data_syns.syns.note = nil
	end

	-- initialize $dial_syn_table
	local dial_syn_table = elements.table()
	:done()

	-- initialize $lang, $main_word, $main_word_link
	-- for $lang="zh:regional", the data modules are named in English,
	-- so the $lang of $main_word is "en";
	-- and the language of the words is "zh", not "zh:regional"
	local lang, lang_qualifier = string.match(args[1], '^(.+):(.+)$')
	lang = lang or args[1]
	local main_word = args[2]
	main_word = mw.ustring.gsub(main_word, "%-[%d]$", "") -- "媽媽-2"→"媽媽"
	local main_word_link = m_links.full_link({
		lang = m_languages.getByCode(lang_qualifier and "en" or lang),
		term = main_word,
		gloss = data_syns.gloss,
	}, "term")

	-- create and add headers for $dial_syn_table
	dial_syn_table
		:node(elements.head_a({
			columns = data_lang.columns,
			title = (data_lang.title):format(main_word_link),
			colour = data_mul.colours["head_a"],
		}))
		:node(elements.row_view_map({
			view_map = dpath_map,
			edit_link = dpath_syns,
			columns = data_lang.columns,
			colour = data_mul.colours["head_a"], -- ?
		}))
		:node(elements.head_b({
			columns = data_lang.columns,
			colour = data_mul.colours["head_a"],
		}))
	:done()

	-- add words to tree
	-- trim tree
	-- ----
	-- enter data tree
	local function recurse(data_variety)
		if #data_variety == 0 then
			-- add words to data tree
			-- first word is an empty string = no words entered
			if (data_syns.syns[data_variety.name]) and (data_syns.syns[data_variety.name][1] ~= '') then
				data_variety.syns = data_syns.syns[data_variety.name]
			end

			-- word fallback with $data_variety.default
			if (data_variety.default) and (not data_variety.syns) then
				if data_variety.default == 'module name' then
					data_variety.syns = {args[2]}
				end
			end

			-- word is n/a
			if (data_variety.syns) and (data_variety.syns[1] == '-') then
				data_variety.code = 'en'
				data_variety.nolink = true
				data_variety.syns[1] = {word = "<i><small>[N/A]</small></i>"}
			end

			-- erase barren leaf from tree
			if (not data_variety.syns) then
				--data_variety = nil
				return
			end

			-- format words
			data_variety.syns = format_syns(data_variety)

			-- prepare header text
			if (not data_variety.text_display) then
				data_variety.text_display = data_variety.english or data_variety.name
				data_variety.text_display = mw.ustring.gsub(data_variety.text_display, '(%(.+%))', '<small>%1</small>')

				if data_variety.code_main == 'ar' and data_variety.wikidata then
					-- XXX: also probably shouldn't be in this module
					-- parenthesise arabic/alt name
					local alt_name = nil
					if not data_variety.suppress_arabic then
						alt_name = mw.wikibase.getLabelByLang(data_variety.wikidata, 'ar')
					elseif data_variety.suppress_arabic ~= 'Y' then
						alt_name = data_variety.suppress_arabic
					end
					alt_name = alt_name and (' (%s)'):format(alt_name) or ''
					data_variety.text_display = data_variety.text_display .. alt_name

					local en_site_link = data_variety.link or mw.wikibase.getSitelink(data_variety.wikidata, 'enwiki')
					--local ar_site_link = nil
					--if not en_site_link then
						--ar_site_link = mw.wikibase.getSitelink(data_variety.wikidata, 'arwiki')
					--end
					site_link = en_site_link
					--if ar_site_link then
						--site_link = 'ar:'..ar_site_link
					--end
					if site_link then
						data_variety.text_display = ('[[w:%s|%s]]'):format(site_link, data_variety.text_display)
					end
				elseif (data_variety.link) then
					data_variety.text_display = ('[[w:%s|%s]]'):format(data_variety.link, data_variety.text_display)
				end
			end
		else
			-- prepare header text
			if (data_variety.name) and (not data_variety.text_display) then
				data_variety.text_display = data_variety.english or data_variety.name
				data_variety.text_display = mw.ustring.gsub(data_variety.text_display, '(%(.+%))', '<small>%1</small>')
				if (data_variety.link) then
					data_variety.text_display = ('[[w:%s|%s]]'):format(data_variety.link, data_variety.text_display)
				end
			end

			-- add fallback iso code
			if not data_variety.code then
				data_variety.code = (data_variety.parent and data_variety.parent.code or lang)
			end

			-- search for tree leaves
			for i, _ in ipairs(data_variety) do
				data_variety[i].parent = data_variety
				data_variety[i].code_main = args[1]
				data_variety[i].code = (data_variety[i].code or data_variety.code)
				data_variety[i].colour = (data_variety[i].colour or data_variety.colour)
				-- recurse
				data_variety[i] = recurse(data_variety[i])
				-- count
				if data_variety[i] then
					if data_variety[i].leaf_count then
						data_variety.leaf_count = (data_variety.leaf_count or 0) + data_variety[i].leaf_count
					else
						data_variety.leaf_count = (data_variety.leaf_count or 0) + 1
					end
				end
			end
			-- erase now-barren sub-tree from tree
			if #m_table.numKeys(data_variety) == 0 then
				data_variety = nil
			end
		end
		return data_variety
	end
	data_lang.varieties = recurse(data_lang.varieties)

	-- add rows to $dial_syn_table
	-- ----
	-- enter data tree
	local function recurse(data_variety, tr)
		-- empty table with no words,
		-- do not try to look for keys, do not try to count keys
		if not data_variety then
			return
		end
		-- if there are no sub-groups
		if #m_table.numKeys(data_variety) == 0 then
			if not tr then
				tr = mw.html.create('tr')
				:done()
			end

			-- add the location + word cells
			tr
				:tag('th')
					:attr('colspan', data_variety.colspan)
					:css("background-color", data_variety.colour)
					:tag('span') -- for CSS `position: sticky`
						:wikitext(data_variety.text_display)
					:done()
				:done()
				:tag('td')
					:css("background-color", data_variety.colour)
					:wikitext(table.concat(data_variety.syns, ', '))
				:done()
			:done()

			dial_syn_table
				:node(tr)
			:done()
		else
			sibling_i = 1
			-- init the row and add headers.
			-- a group header (the thing that has rowspan) must belong to a tr of a leaf, =
			-- but only if that leaf is the first leaf among its siblings.
			-- imagine the formatting of native mediawiki tables
			-- (not tr): as when recursing for the first time, or when it has been 'nil'-ed
			-- (data_variety.parent): don't generate a th for the root. empty, useless and unwanted.
			if (not tr) and (data_variety.parent) then
				tr = mw.html.create('tr')
				:done()
			end
			if (tr) and (sibling_i == 1) then
				tr
					:tag('th')
						:attr('rowspan', data_variety.leaf_count)
						:attr('colspan', data_variety.colspan)
						:css("background-color", data_variety.colour)
						:tag('span') -- for CSS `position: sticky`
							:wikitext(data_variety.text_display)
						:done()
					:done()
				:done()
			end
			-- search for tree leaves
			-- use sparseIpairs() because the deletion of things above leaves gaps in the array
			-- and lua does not continue looping if i + 1 does not exist
			-- use sibling_i because sparseIpairs() provides the original i, not necessarily beginning at 1
			for i, _ in m_table.sparseIpairs(data_variety) do
				if sibling_i > 1 then
					tr = nil
				end
				data_variety[i] = recurse(data_variety[i], tr)
				sibling_i = sibling_i + 1
			end
		end
		return data_variety
	end
	recurse(data_lang.varieties)

	-- add notes to $dial_syn_table
	-- ----
	-- insert synonyms note
	if data_syns.note and data_syns.note ~= "" then
		table.insert(data_lang.notes, 1, data_syns.note)
	end
	-- add notes to $dial_syn_table
	for _, note in ipairs(data_lang.notes) do
		dial_syn_table
			:tag('tr')
				:tag('td')
					:attr("colspan", #data_lang.columns)
					:wikitext(note)
				:done()
			:done()
		:done()
	end

	-- return
	return tostring(dial_syn_table) .. require("Module:TemplateStyles")("Template:dialect synonyms/styles.css")
end

return export