Jump to content

Module:dialect synonyms

Wiktionary වෙතින්

See {{dialect synonyms}}.


-- Module:dialect synonyms
-- Generates dialectal synonym tables for various languages.

local export = {}

local m_links = require("Module:links")
local m_languages = require("Module:languages")
local m_table = require("Module:table")
local m_param_utils = require("Module:parameter utilities")

local PARAM_MODS = {
	group = { type = "string" },
	note = { type = "string" },
	ipa = { type = "string" },

	alt = {},
	t = { item_dest = "gloss" },
	gloss = {},
	tr = {},
	ts = {},
	g = { item_dest = "genders", sublist = true },
	pos = {},
	lit = {},
	id = {},
	sc = { type = "script" },

	q = { type = "qualifier" },
	qq = { type = "qualifier" },
	l = { type = "labels" },
	ll = { type = "labels" },
}

local TERM_NA = "—"

local langs = {}
local handler_cache = {}

-- TODO: 3 collapsing modes? collapsed, uncollapsed (all locations matching the current entry title), uncollapsed (all)
-- Create the main table
local function __create_table()
	return mw.html.create("table")
		:addClass("dial-syn")
		:addClass("wikitable")
		:addClass("mw-collapsible")
		:addClass("mw-collapsed")
		:done()
end

-- Create the title row of the table
local function __create_title_row(columns, title, colour)
	return mw.html.create("tr")
		:tag("th")
		:attr("colspan", #columns)
		:css("background-color", colour)
		:wikitext(title)
		:done()
		:done()
end

-- Create the header row with column names
local function __create_columns_row(columns, colour)
	local tr = mw.html.create("tr"):done()
	for _, col in ipairs(columns) do
		tr:tag("th")
			:css("background-color", colour)
			:wikitext(col)
			:done()
	end
	return tr
end

-- Create the row with "view map" and "edit data" links
local function __create_view_map_row(columns, colour, view_map, edit_link)
	return mw.html.create("tr")
		:tag("td")
		:attr("colspan", #columns)
		:css("text-align", "right")
		:css("background-color", colour)
		:wikitext(("[[%s|view map]]; [[%s|edit data]]"):format(view_map, edit_link))
		:done()
		:done()
end

-- Helper to generate a link to create a missing data module
local function __prompt_create_data(module_path, preload_path)
	local url = mw.uri.fullUrl(module_path, { action = 'edit', preload = preload_path })
	return ("→Create [%s %s]?"):format(tostring(url), module_path)
end

-- Retrieve or load a language-specific handler module
local function __get_handler(code_main)
	if not code_main then return nil end
	if handler_cache[code_main] == nil then
		local path = "Module:dialect synonyms/handlers/" .. code_main
		handler_cache[code_main] = mw.title.new(path).exists and require(path) or false
	end
	return handler_cache[code_main] or nil
end
export.get_handler = __get_handler

function export.collect_aliases(varieties)
	local alias_map = {}
	local function __collect_recursive(node)
		if node.name then
			alias_map[node.name] = node.name
		end
		if node.aliases then
			if type(node.aliases) == "table" then
				for _, alias in ipairs(node.aliases) do
					alias_map[alias] = node.name
				end
			elseif type(node.aliases) == "string" then
				alias_map[node.aliases] = node.name
			end
		end
		for _, child in ipairs(node) do
			__collect_recursive(child)
		end
	end
	for _, variety in ipairs(varieties) do
		__collect_recursive(variety)
	end
	return alias_map
end

-- Process a single term object using the handler and standard normalization techniques.
-- This handles legacy syntax parsing (e.g. term:note) and applies language-specific logic.
function export.process_term(data_variety, data, invalid_langs)
	-- Re-use language objects if possible to avoid overhead
	if not langs[data_variety.code] then
		local lang = m_languages.getByCode(data_variety.code, nil, true)
		if not lang then
			if invalid_langs then
				table.insert(invalid_langs, data_variety.code)
			end
			lang = m_languages.getByCode(data_variety.code_main)
		elseif lang:hasType("etymology-only") then
			lang = lang:getFull()
		end
		langs[data_variety.code] = lang
	end

	data.lang = data.lang or langs[data_variety.code]
	data.alt = data.alt or data.term

	if data_variety.nolink then data.term = nil end

	-- Language-specific processing via handlers (e.g. stripping Etymology N numbers)
	local handler = __get_handler(data_variety.code_main)
	if handler and handler.process then
		data = handler.process(data_variety, data)
	end
	
	return data
end

-- Format a processed term object into a final string for display (link + qualifiers).
-- This uses Module:links to generate the standard Wiktionary link.
function export.format_term(data_variety, data)
	if type(data) == "string" then return data end -- Already formatted or special string (e.g. TERM_NA)

	-- Convert q to qq so qualifiers appear after the term
	if data.q and not data.qq then
		data.qq = data.q
		data.q = nil
	elseif data.q and data.qq then
		-- Merge q into qq if both exist
		if type(data.q) ~= "table" then data.q = {data.q} end
		if type(data.qq) ~= "table" then data.qq = {data.qq} end
		for _, v in ipairs(data.q) do
			table.insert(data.qq, v)
		end
		data.q = nil
	end

	local word
	local handler = __get_handler(data_variety.code_main)
	if handler and handler.format_term then
		word = handler.format_term(data_variety, data)
	elseif handler and handler.make_link then
		word = handler.make_link(data)
	else
		-- Pass data with show_qualifiers to let full_link handle qualifiers automatically
		local link_data = {}
		for k, v in pairs(data) do
			link_data[k] = v
		end
		if data.qq then
			link_data.show_qualifiers = true
		end
		word = m_links.full_link(link_data)
	end

	return word
end

-- Format the display text for a variety or location (e.g. making parens small).
local function __format_text_display(data_variety, is_leaf)
	if data_variety.text_display then
		return data_variety
	end

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

	-- Language-specific display formatting via handlers (leaf nodes only)
	if is_leaf then
		local handler = __get_handler(data_variety.code_main)
		if handler and handler.format_display then
			data_variety = handler.format_display(data_variety)
		end
	end

	-- Fallback: simple Wikipedia link
	if (not data_variety.text_display_formatted) and data_variety.link then
		data_variety.text_display = ('[[w:%s|%s]]'):format(data_variety.link, data_variety.text_display)
	end

	return data_variety
end

-- Main data fetching function.
function export.get_data(lang_code, term, id, demo_mode, skip_validation)
	local module_path = "Module:dialect synonyms"

	local synonym_data_path
	local handler = __get_handler(lang_code)
	if handler and handler.get_synonym_data_path then
		synonym_data_path = handler.get_synonym_data_path(lang_code, term, id)
	end
	
	local language_data_path = module_path .. "/" .. lang_code

	local term_path = term .. (id and id ~= "" and "/" .. id or "")
	if not synonym_data_path then
		synonym_data_path = language_data_path .. "/" .. term_path
	end

	-- Set defaults
	local language_data_req = mw.title.new(language_data_path).exists and require(language_data_path) or nil
	local synonym_data_req = mw.title.new(synonym_data_path).exists and require(synonym_data_path) or nil

	if demo_mode then
		synonym_data_path = language_data_path
		synonym_data_req = {}
		synonym_data_req.syns = {}
	end

	if (not language_data_req) or (not synonym_data_req) then
		return nil, language_data_path, synonym_data_path -- Return paths for create prompts
	end

	if handler and handler.get_map_params then
		local params = handler.get_map_params(language_data_req, synonym_data_req)
		for k, v in pairs(params) do
			language_data_req[k] = v
		end
	end

	language_data_req.title = language_data_req.title or "Dialectal synonyms of %s"
	language_data_req.columns = language_data_req.columns or { "Variety", "Location", "Words" }
	language_data_req.notes = language_data_req.notes or {}
	language_data_req.sources = language_data_req.sources or {}
	language_data_req.note_aliases = language_data_req.note_aliases or {}

	local collected_notes = {}
	local collected_notes_set = {}

	if synonym_data_req.note then
		local notes = type(synonym_data_req.note) == "string" and { synonym_data_req.note } or synonym_data_req.note
		for _, note_key in ipairs(notes) do
			if note_key ~= "" and not collected_notes_set[note_key] then
				collected_notes_set[note_key] = true
				table.insert(collected_notes, note_key)
			end
		end
	end

	if synonym_data_req.notes then
		for _, note_key in ipairs(synonym_data_req.notes) do
			if note_key ~= "" and not collected_notes_set[note_key] then
				collected_notes_set[note_key] = true
				table.insert(collected_notes, note_key)
			end
		end
	end

	local expanded_sources = {}
	if synonym_data_req.source then
		for _, src_key in ipairs(synonym_data_req.source) do
			local expanded = language_data_req.sources[src_key]
			if expanded then
				table.insert(expanded_sources, expanded)
			elseif src_key ~= "" then
				table.insert(expanded_sources, src_key)
			end
		end
	end
	local combined_sources = #expanded_sources > 0 and ("'''Sources:''' " .. table.concat(expanded_sources, "; ")) or nil
	
	local allowed_keys = {
		["syns"] = true,
		["gloss"] = true,
		["meaning"] = true,
		["title"] = true,
		["note"] = true,
		["notes"] = true,
		["source"] = true,
	}

	-- Validation tracking
	local used_keys = {}
	local duplicate_usage = {}
	local duplicate_terms = {}
	local invalid_langs = {}
	local is_module_ns = mw.title.getCurrentTitle().nsText == "Module"
	if skip_validation then is_module_ns = false end

	local alias_map = export.collect_aliases(language_data_req.varieties)

	if synonym_data_req.syns then
		for alias, canonical in pairs(alias_map) do
			if alias ~= canonical and synonym_data_req.syns[alias] then
				synonym_data_req.syns[canonical] = synonym_data_req.syns[canonical] or {}
				for _, term in ipairs(synonym_data_req.syns[alias]) do
					table.insert(synonym_data_req.syns[canonical], term)
				end
				used_keys[alias] = true
			end
		end
	end

	local function __populate_tree(data_variety)
		local new_node = m_table.shallowCopy(data_variety)

		if #new_node == 0 then
			-- Leaf node
			if (synonym_data_req.syns[new_node.name]) then
				if is_module_ns then
					if used_keys[new_node.name] then
						table.insert(duplicate_usage, new_node.name)
					end
					used_keys[new_node.name] = true

					-- Check for duplicate terms within the same list
					local seen_terms = {}
					for _, t in ipairs(synonym_data_req.syns[new_node.name]) do
						if seen_terms[t] then
							if not duplicate_terms[new_node.name] then
								duplicate_terms[new_node.name] = {}
							end
							local already_added = false
							for _, dt in ipairs(duplicate_terms[new_node.name]) do
								if dt == t then already_added = true break end
							end
							if not already_added then
								table.insert(duplicate_terms[new_node.name], t)
							end
						end
						seen_terms[t] = true
					end
				end

				if (synonym_data_req.syns[new_node.name][1] ~= '') then
					new_node.syns = synonym_data_req.syns[new_node.name]
				end
			end

			if demo_mode then
				new_node.syns = { '-' }
			end

			if (new_node.default) and (not new_node.syns) then
				if new_node.default == 'module name' then
					new_node.syns = { term } -- default fallback
				end
			end

			if (new_node.syns) and (new_node.syns[1] == '-') then
				new_node.nolink = true
				new_node.syns[1] = TERM_NA
			end

			if (not new_node.syns) then
				return nil
			end

			-- Process synonyms
			local terms = new_node.syns
			local terms_processed = {}
			
			-- Special case for TERM_NA
			if terms[1] == TERM_NA and #terms == 1 then
				terms_processed = terms
			else
				local simple_processing = true
				for _, term_str in ipairs(terms) do
					if type(term_str) == "string" and term_str:find("[<,;]") then
						simple_processing = false
						break
					end
				end

				if simple_processing then
					for _, term_entry in ipairs(terms) do
						local term_obj
						if type(term_entry) == "table" then
							term_obj = term_entry
						else
							term_obj = { term = term_entry }
						end
						table.insert(terms_processed, export.process_term(new_node, term_obj, invalid_langs))
					end
				else
					local masked_terms = {}
					for _, term_str in ipairs(terms) do
						local masked_term = term_str:gsub("<(%/?sup[^>]*)>", "\1%1\2")
						table.insert(masked_terms, masked_term)
					end

					local parsed_terms = m_param_utils.parse_list_with_inline_modifiers_and_separate_params({
						processed_args = { masked_terms },
						param_mods = PARAM_MODS,
						termarg = 1,
						track_module = "dialect synonyms",
					})
					
					for _, term_obj in ipairs(parsed_terms) do
						if term_obj.term then
							term_obj.term = term_obj.term:gsub("\1", "<"):gsub("\2", ">")
						end
						if term_obj.note and not collected_notes_set[term_obj.note] then
							collected_notes_set[term_obj.note] = true
							table.insert(collected_notes, term_obj.note)
						end
						if term_obj.q then
							for i, qual in ipairs(term_obj.q) do
								local alias = qual:match("^%[(.+)%]$")
								if alias then
									term_obj.q[i] = alias
									if not collected_notes_set[alias] then
										collected_notes_set[alias] = true
										table.insert(collected_notes, alias)
									end
								end
							end
						end
						table.insert(terms_processed, export.process_term(new_node, term_obj, invalid_langs))
					end
				end
			end
			
			new_node.syns = terms_processed
			new_node = __format_text_display(new_node, true)
			return new_node
		else
			-- Branch node
			new_node = __format_text_display(new_node, false)

			if not new_node.code then
				new_node.code = (new_node.parent and new_node.parent.code or lang_code)
			end

			local valid_children = {}
			for i, child_raw in ipairs(data_variety) do
				local child_clone = m_table.shallowCopy(child_raw)
				child_clone.parent = new_node -- Point to the new parent
				child_clone.code_main = lang_code
				child_clone.code = (child_clone.code or new_node.code)
				child_clone.colour = (child_clone.colour or new_node.colour)
				
				local populated_child = __populate_tree(child_clone)

				if populated_child then
					if populated_child.leaf_count then
						new_node.leaf_count = (new_node.leaf_count or 0) + populated_child.leaf_count
					else
						new_node.leaf_count = (new_node.leaf_count or 0) + 1
					end
					table.insert(valid_children, populated_child)
				end
			end

			for i = #new_node, 1, -1 do
				new_node[i] = nil
			end
			for i, child in ipairs(valid_children) do
				new_node[i] = child
			end

			if #valid_children == 0 then
				return nil
			end
			
			return new_node
		end
	end

	local populated_varieties = __populate_tree(language_data_req.varieties)

	local expanded_notes = {}
	for _, note_key in ipairs(collected_notes) do
		if note_key == "" then
			-- Skip empty
		elseif note_key:match("^%[(.+)%]$") then
			local alias = note_key:match("^%[(.+)%]$")
			local expanded = language_data_req.note_aliases[alias] or (language_data_req.notes and language_data_req.notes[alias])
			if expanded then
				table.insert(expanded_notes, expanded)
			else
				table.insert(expanded_notes, alias)
			end
		else
			local expanded = language_data_req.note_aliases[note_key] or (language_data_req.notes and language_data_req.notes[note_key])
			if expanded then
				table.insert(expanded_notes, expanded)
			else
				table.insert(expanded_notes, note_key)
			end
		end
	end
	local combined_notes = #expanded_notes > 0 and ("'''Notes:''' " .. table.concat(expanded_notes, " ")) or nil
	
	-- Post-processing validation results
	local validation = {
		unused_keys = {},
		invalid_keys = {},
		invalid_langs = invalid_langs,
		duplicate_usage = duplicate_usage,
		duplicate_terms = duplicate_terms
	}
	
	if is_module_ns then
		for loc, _ in pairs(synonym_data_req.syns) do
			if not used_keys[loc] then
				table.insert(validation.unused_keys, loc)
			end
		end
		
		for key, _ in pairs(synonym_data_req) do
			if not allowed_keys[key] then
				table.insert(validation.invalid_keys, key)
			end
		end
	end

	local root_path = mw.title.getCurrentTitle().prefixedText == language_data_path

	return {
		varieties = populated_varieties,
		properties = {
			title = (root_path and "Dialectal varieties") or language_data_req.title,
			columns = language_data_req.columns,
			notes = language_data_req.notes,
			combined_notes = combined_notes,
			combined_sources = combined_sources,
			gloss = (synonym_data_req.gloss ~= "" and synonym_data_req.gloss) or (synonym_data_req.meaning ~= "" and synonym_data_req.meaning) or nil,
			map_template_path = "Template:dialect map/" .. lang_code .. "/" .. (root_path and ".all" or term_path),
			synonym_data_path = synonym_data_path,
		},
		validation = validation
	}
end

function export.show(frame)
	local params = {
		[1] = { required = true, default = "und" },
		[2] = { default = mw.loadData("Module:headword/data").pagename },
		['id'] = {},
		['dpath syns'] = {}, -- Keeping param name for compatibility
		['demo mode'] = { type = "boolean" },
	}
	local args = require("Module:parameters").process(frame:getParent().args, params, nil, "dialect synonyms", "show")

	local lang_code = args[1]
	local term = args[2]
	local id = args['id']
	
	local dataset, language_data_path, synonym_data_path = export.get_data(lang_code, term, id, args['demo mode'])

	if not dataset then
		if language_data_path and synonym_data_path then
			local has_lang = mw.title.new(language_data_path).exists
			if not has_lang then
				return __prompt_create_data(language_data_path, 'Module:dialect synonyms/und')
			else
				return __prompt_create_data(synonym_data_path, 'Module:dialect synonyms/' .. lang_code .. '/')
			end
		else
			return "Error loading data."
		end
	end
	
	local varieties = dataset.varieties
	local props = dataset.properties
	local validation = dataset.validation

	if not varieties then
		return "No varieties found."
	end

	local dialect_synonyms_table
	local handler = __get_handler(lang_code)

	if handler and handler.create_table then
		dialect_synonyms_table = handler.create_table()
	else
		dialect_synonyms_table = __create_table()
	end
	dialect_synonyms_table = dialect_synonyms_table:done()

	local lang, lang_qualifier = string.match(args[1], '^(.+):(.+)$')
	lang = lang or args[1]
	
	local main_word_link = m_links.full_link({
		lang = m_languages.getByCode(lang_qualifier and "en" or lang),
		term = term,
		gloss = props.gloss,
		id = id,
	}, "term")

	local header_colour = 'var(--wikt-palette-green-0)'
	
	local title_row
	if handler and handler.create_title_row then
		title_row = handler.create_title_row(props.columns, (props.title):format(main_word_link), header_colour)
	else
		title_row = __create_title_row(props.columns, (props.title):format(main_word_link), header_colour)
	end
	dialect_synonyms_table:node(title_row)
	
	local map_row
	if handler and handler.create_map_row then
		map_row = handler.create_map_row(props.columns, header_colour, props.map_template_path, props.synonym_data_path)
	else
		map_row = __create_view_map_row(props.columns, header_colour, props.map_template_path, props.synonym_data_path)
	end
	dialect_synonyms_table:node(map_row)

	local columns_row
	if handler and handler.create_columns_row then
		columns_row = handler.create_columns_row(props.columns, header_colour)
	else
		columns_row = __create_columns_row(props.columns, header_colour)
	end
	dialect_synonyms_table:node(columns_row)
	dialect_synonyms_table:done()

	-- Recursive function to render the tree of varieties into HTML rows
	local function __render_tree(data_variety, tr)
		if not data_variety then return end

		if #m_table.numKeys(data_variety) == 0 then
			-- Leaf node
			if not tr then
				tr = mw.html.create('tr'):done()
			end

			local syns_text_list = {}
			for _, item in ipairs(data_variety.syns) do
				table.insert(syns_text_list, export.format_term(data_variety, item))
			end
			local syns_str = table.concat(syns_text_list, ', ')

			tr
				:tag('th')
					:attr('colspan', data_variety.colspan)
					:attr('data-lat', data_variety.lat)
					:attr('data-lon', data_variety.long)
					:attr('data-term', data_variety.text_display)
					:css("background-color", data_variety.colour)
					:tag('span')
						:wikitext(data_variety.text_display)
						:done()
					:done()
				-- TODO: fully commit to [[WT:Palette]] and output `-0` colors for normal table cells?
				:tag('td')
					:css("background-color", data_variety.colour)
					:wikitext(syns_str)
					:done()
				:done()

			dialect_synonyms_table:node(tr):done()
		else
			-- Branch node
			if (not tr) and (data_variety.parent) then
				tr = mw.html.create('tr'):done()
			end
			if tr then
				tr
					:tag('th')
						:attr('rowspan', data_variety.leaf_count)
						:attr('colspan', data_variety.colspan)
						:css("background-color", data_variety.colour)
						:tag('span')
							:wikitext(data_variety.text_display)
							:done()
						:done()
					:done()
			end
			local first = true
			for i, _ in m_table.sparseIpairs(data_variety) do
				if not first then tr = nil end
				__render_tree(data_variety[i], tr)
				first = false
			end
		end
	end
	
	if handler and handler.render_table_content then
		handler.render_table_content(varieties, dialect_synonyms_table)
	else
		__render_tree(varieties)
	end
	
	-- Output validation warnings (unused keys, duplicates)
	local validation_errors = {}
	if #validation.unused_keys > 0 then
		table.sort(validation.unused_keys)
		table.insert(validation_errors, {
			track = "dialect synonyms/unused key",
			warning = "Warning: The following locations are not present in the varieties data:",
			items = validation.unused_keys
		})
	end
	if #validation.invalid_keys > 0 then
		table.sort(validation.invalid_keys)
		table.insert(validation_errors, {
			track = "dialect synonyms/invalid key",
			warning = "Warning: The following keys are not allowed in the dataset:",
			items = validation.invalid_keys
		})
	end
	if #validation.invalid_langs > 0 then
		table.sort(validation.invalid_langs)
		table.insert(validation_errors, {
			track = "dialect synonyms/invalid lang code",
			warning = "Warning: The following language codes are invalid (falling back to main language):",
			items = validation.invalid_langs
		})
	end
	if #validation.duplicate_usage > 0 then
		table.sort(validation.duplicate_usage)
		table.insert(validation_errors, {
			track = "dialect synonyms/duplicate key usage",
			warning = "Warning: The following locations are used multiple times in the varieties data:",
			items = validation.duplicate_usage
		})
	end
	if next(validation.duplicate_terms) then
		local items = {}
		for loc, terms in pairs(validation.duplicate_terms) do
			table.insert(items, loc .. ": " .. table.concat(terms, ", "))
		end
		table.sort(items)
		table.insert(validation_errors, {
			track = "dialect synonyms/duplicate terms",
			warning = "Warning: The following locations contain duplicate terms:",
			items = items
		})
	end

	if #validation_errors > 0 then
		local track = require("Module:debug/track")
		local warning_div = mw.html.create("div")
			:css("background-color", "var(--wikt-palette-red-1)")
			:css("border", "1px solid var(--wikt-palette-red-5)")
			:css("padding", "0.5em")
			:css("margin", "0.5em 0")

		for _, err in ipairs(validation_errors) do
			track(err.track)
			warning_div:tag("strong"):wikitext(err.warning):done()
			local ul = warning_div:tag("ul"):css("margin-left", "1.5em")
			for _, item in ipairs(err.items) do
				ul:tag("li"):wikitext(item):done()
			end
		end
		dialect_synonyms_table:node(warning_div:done())
	end

	-- Append combined notes row
	local all_notes = {}
	for _, note in ipairs(props.notes) do
		table.insert(all_notes, note)
	end
	if props.combined_notes then
		table.insert(all_notes, props.combined_notes)
	end
	if #all_notes > 0 then
		local notes_text = table.concat(all_notes, "<br>")
		dialect_synonyms_table:tag('tr'):tag('td'):attr("colspan", #props.columns):wikitext(notes_text):done():done():done()
	end

	if props.combined_sources then
		dialect_synonyms_table:tag('tr'):tag('td'):attr("colspan", #props.columns):css("font-size", "90%"):wikitext(props.combined_sources):done():done():done()
	end

	if props.map_template_path and mw.title.getCurrentTitle().nsText == "Module" then
		local data_module_path = "Module:dialect map/data/" .. lang_code
		
		if mw.title.new(data_module_path).exists then
			local map_title = mw.title.new(props.map_template_path)
			if map_title and not map_title.exists then
				require("Module:debug/track")("dialect synonyms/missing map")
			end
		end
	end

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

return export
"https://si.wiktionary.org/w/index.php?title=Module:dialect_synonyms&oldid=228617" වෙතින් සම්ප්‍රවේශනය කෙරිණි