Jump to content

မေႃႇၵျူး:senseno

လုၵ်ႉတီႈ ဝိၵ်ႇသျိၼ်ႇၼရီႇ မႃး

Documentation for this module may be created at မေႃႇၵျူး:senseno/doc

local export = {}

local M = require("Module:module loader").init({
	require = {
		string_utilities = "Module:string utilities",
		template_parser = "Module:template parser",
		languages = "Module:languages",
		parameters = "Module:parameters",
	},
})

local unpack = unpack or table.unpack -- Lua 5.2 compatibility

local SENSE_LABEL = "sense"
local ETYMOLOGY_LABEL = "etymology"
local SENSE_INDEX_HIDE_LABEL = "-"

local TEMPLATE_TO_TYPE = {}
for template_type, template_names in pairs{
	etymon = { "etymon", "ety" },
	etymid = { "etymid" },
	senseid = { "senseid", "sid" },
	tcl = { "tcl", "transclude sense", "transclude" },
} do
	for _, name in ipairs(template_names) do
		TEMPLATE_TO_TYPE[name] = template_type
	end
end

local PRIORITY_ORDER = { "senseid", "tcl", "etymon", "etymid" }
local MATCH_PRIORITY = {}
for priority_index, template_type in ipairs(PRIORITY_ORDER) do
	MATCH_PRIORITY[template_type] = #PRIORITY_ORDER + 1 - priority_index
end

local ETYMOLOGY_TYPES = { etymon = true, etymid = true }

local IGNORE_HEADERS = {
	["pronunciation"] = true,
	["references"] = true,
	["further reading"] = true,
	["translations"] = true,
	["anagrams"] = true,
	["trivia"] = true,
	["usage notes"] = true,
	["synonyms"] = true,
	["antonyms"] = true,
	["hyponyms"] = true,
	["hypernyms"] = true,
	["related terms"] = true,
	["derived terms"] = true,
	["coordinate terms"] = true,
	["declension"] = true,
	["conjugation"] = true,
	["alternative forms"] = true,
	["see also"] = true,
	["descendants"] = true,
	["etymology"] = true,
}

local function heading_level(raw_heading)
	local start_pos, end_pos = raw_heading:find("^=+")
	return start_pos and (end_pos - start_pos + 1) or 0
end

local function heading_text(raw_heading)
	return M.string_utilities.trim((raw_heading:match("^=+%s*(.-)%s*=+$") or raw_heading))
end

local function heading_clean_key(raw_heading)
	local text = heading_text(raw_heading):lower()
	if not text:match("^etymology%s+%d") then
		text = text:gsub("%s+%d+$", "")
	end
	return text
end

local function is_etymology_heading(text)
	return text and text:match("^[Ee]tymology")
end

local function count_senses(text)
	local lines = M.string_utilities.gmatch(text, "\n#[#*:]*")
	local senseno = {}
	local line = lines()
	while line ~= nil do
		if not line:find(":") and not line:find("%*") then
			line = line:match("#+")
			local indentation = line and #line or 0
			if indentation > #senseno then
				table.insert(senseno, 1)
			else
				senseno = { unpack(senseno, 1, indentation) }
				senseno[#senseno] = senseno[#senseno] + 1
			end
		end
		line = lines()
	end
	return senseno
end

local function parsed_slice_to_string(parsed, from_idx, to_idx)
	local parts = {}
	for idx = from_idx, to_idx do
		parts[#parts + 1] = tostring(parsed[idx])
	end
	return table.concat(parts)
end

local parsed_content = {}

local function get_page_content(page_title)
	local current = mw.title.getCurrentTitle()
	if page_title == current.prefixedText then
		return current:getContent()
	end
	local title_obj = mw.title.new(page_title)
	return title_obj and title_obj:getContent() or nil
end

local function get_parsed_content(title)
	if not parsed_content[title] then
		local content = get_page_content(title)
		if not content then
			error("Could not read content of page [[" .. title .. "]] (page may not exist or has no content)")
		end
		parsed_content[title] = M.template_parser.parse(content)
	end
	return parsed_content[title]
end

local function is_heading(node)
	return M.template_parser.class_else_type(node) == "heading"
end

local function get_arg_text(arg)
	if arg == nil then return nil end
	if type(arg) == "table" and arg.expand then
		return tostring(arg)
	end
	return arg
end

local function get_id_arg(args)
	if not args then return nil end
	local id_arg = args["id"] or args.id
	if id_arg then return id_arg end
	for key, value in pairs(args) do
		if type(key) == "string" and M.string_utilities.lower(M.string_utilities.trim(key)) == "id" then
			return value
		end
	end
	return nil
end

local function match_etymon(args, id, get_text)
	local id_arg = get_id_arg(args)
	local trimmed_id = id_arg and M.string_utilities.trim(tostring(get_text(id_arg)))
	return trimmed_id and trimmed_id ~= "" and trimmed_id == M.string_utilities.trim(id)
end

local function match_etymid(args, id, get_text)
	local arg1 = args and args[1]
	return get_text(arg1) == id
end

local function normalize_id_for_compare(s)
	return s and (s:gsub("_", " ")) or nil
end

local function match_senseid(args, code, id, get_text)
	local arg1, arg2 = args and args[1], args and args[2]
	local lang_arg = get_text(arg1)
	local id_arg = normalize_id_for_compare(get_text(arg2))
	return lang_arg == code and id_arg == normalize_id_for_compare(id)
end

local function match_tcl(args, code, id, get_text)
	local arg1 = args and args[1]
	if get_text(arg1) ~= code then return false end
	local id_arg = get_id_arg(args)
	if not id_arg then return false end
	local id_str = M.string_utilities.trim(tostring(get_text(id_arg)))
	if id_str == "" then return false end
	for part in M.string_utilities.gsplit(id_str, ",") do
		if M.string_utilities.trim(part) == id then return true end
	end
	return false
end

local function template_matches(name, args, code, id, get_text)
	local template_type = TEMPLATE_TO_TYPE[name]
	if not template_type then return nil end
	if template_type == "etymon" then return match_etymon(args, id, get_text) and template_type end
	if template_type == "etymid" then return match_etymid(args, id, get_text) and template_type end
	if template_type == "senseid" then return match_senseid(args, code, id, get_text) and template_type end
	if template_type == "tcl" then return match_tcl(args, code, id, get_text) and template_type end
	return nil
end

local function compute_ambiguity(parsed, lang_name)
	local in_lang_section = false
	local distinct_pos_count = 0

	for _, node in ipairs(parsed) do
		if is_heading(node) then
			local raw_heading = tostring(node)
			local level = heading_level(raw_heading)
			local text = heading_text(raw_heading)

			if level == 2 then
				in_lang_section = (text == lang_name)
			elseif in_lang_section and level > 2 then
				local key = heading_clean_key(raw_heading)
				if not IGNORE_HEADERS[key] and not key:match("^etymology") then
					distinct_pos_count = distinct_pos_count + 1
				end
			end
		end
	end

	return distinct_pos_count > 1
end

local function find_best_template_match(parsed, code, id, get_text)
	local best_priority, best_match_index, best_last_heading, best_type = 0, nil, nil, nil
	local last_heading_index = 0

	for idx, node in ipairs(parsed) do
		local node_type = M.template_parser.class_else_type(node)

		if node_type == "heading" then
			last_heading_index = idx
		elseif node_type == "template" then
			local name = node:get_name()
			if TEMPLATE_TO_TYPE[name] and node.get_arguments then
				local args = node:get_arguments()
				local match_type = template_matches(name, args, code, id, get_text)
				if match_type and MATCH_PRIORITY[match_type] > best_priority then
					best_priority = MATCH_PRIORITY[match_type]
					best_match_index = idx
					best_last_heading = last_heading_index
					best_type = match_type
				end
			end
		end
	end

	return best_match_index, best_last_heading, best_type
end

local function find_parent_heading(parsed, heading_i, level)
	if level <= 1 then return 0, 0 end
	for idx = heading_i - 1, 1, -1 do
		local prev = parsed[idx]
		if is_heading(prev) then
			local raw_heading = tostring(prev)
			local prev_level = heading_level(raw_heading)
			if prev_level < level then
				return idx, prev_level
			end
		end
	end
	return 0, 0
end

local function find_scope_end(parsed, parent_index, parent_level)
	if parent_index > 0 then
		for idx = parent_index + 1, #parsed do
			local node = parsed[idx]
			if is_heading(node) then
				local node_level = heading_level(tostring(node))
				if node_level <= parent_level then
					return idx - 1
				end
			end
		end
	end
	return #parsed
end

local function find_etymology_header(parsed, heading_i, level)
	if level <= 1 then return "" end
	for idx = heading_i - 1, 1, -1 do
		local prev = parsed[idx]
		if is_heading(prev) then
			local prev_raw = tostring(prev)
			if heading_level(prev_raw) < level then
				local prev_text = heading_text(prev_raw)
				if is_etymology_heading(prev_text) then
					return prev_text
				end
				break
			end
		end
	end
	return ""
end

local function build_result_for_match(parsed, match_i, heading_i, is_heading_fn)
	local pos_header, etym_header = "", ""
	local siblings_count, my_index = 0, 0
	local pos_count_under_etymology = 0

	if heading_i <= 0 then
		local slice_str = parsed_slice_to_string(parsed, heading_i + 1, match_i - 1)
		local senseno_list = count_senses(slice_str)
		return table.concat(senseno_list, "."), ""
	end

	local raw_heading = tostring(parsed[heading_i])
	pos_header = heading_text(raw_heading)
	local level = heading_level(raw_heading)
	local clean_key = heading_clean_key(raw_heading)

	local parent_index, parent_level = find_parent_heading(parsed, heading_i, level)
	local scope_end = find_scope_end(parsed, parent_index, parent_level)

	for idx = parent_index + 1, scope_end do
		local node = parsed[idx]
		if is_heading_fn(node) then
			local raw_heading = tostring(node)
			if heading_level(raw_heading) == level then
				pos_count_under_etymology = pos_count_under_etymology + 1
				if heading_clean_key(raw_heading) == clean_key then
					siblings_count = siblings_count + 1
					if idx == heading_i then
						my_index = siblings_count
					end
				end
			end
		end
	end

	if siblings_count > 1 then
		pos_header = pos_header .. " " .. my_index
	end

	etym_header = find_etymology_header(parsed, heading_i, level)

	if etym_header ~= "" and pos_count_under_etymology == 1 then
		pos_header = ""
	end

	if etym_header ~= "" then
		pos_header = (pos_header ~= "") and (etym_header .. ", " .. pos_header) or etym_header
	end

	local slice_str = parsed_slice_to_string(parsed, heading_i + 1, match_i - 1)
	local senseno_list = count_senses(slice_str)
	return table.concat(senseno_list, "."), pos_header
end

local function adjust_sense_index_for_tcl(sense_index, id_str, id)
	local position = 1
	for part in M.string_utilities.gsplit(id_str, ",") do
		if M.string_utilities.trim(part) == id then break end
		position = position + 1
	end
	if position <= 1 then return sense_index end

	local parts = {}
	for part in M.string_utilities.gsplit(sense_index, "%.") do
		parts[#parts + 1] = tonumber(part) or 1
	end
	if #parts > 0 then
		parts[#parts] = parts[#parts] + (position - 1)
		return table.concat(parts, ".")
	end
	return sense_index
end

local function find_senseid(title, code, id)
	local parsed = get_parsed_content(title)
	local lang = M.languages.getByCode(code)
	local lang_name = lang:getCanonicalName()

	local ambiguous = compute_ambiguity(parsed, lang_name)
	local best_match_index, best_last_heading, best_type = find_best_template_match(parsed, code, id, get_arg_text)

	if not best_match_index then
		local template_list = table.concat(PRIORITY_ORDER, ", ")
		local message = "Could not find " .. template_list .. " template in the entry [[" .. title
			.. "]] (with language " .. code .. " and id '" .. id
			.. "'); this could be because you are in preview mode and the template has not yet been saved to the page"
		error(message)
	end

	local sense_index, pos_header = build_result_for_match(parsed, best_match_index, best_last_heading, is_heading)

	if best_type == "tcl" then
		local template_node = parsed[best_match_index]
		local template_args = template_node.get_arguments and template_node:get_arguments()
		local id_arg = template_args and get_id_arg(template_args)
		local id_str = id_arg and M.string_utilities.trim(tostring(get_arg_text(id_arg))) or ""
		sense_index = adjust_sense_index_for_tcl(sense_index, id_str, id)
	end

	return sense_index, pos_header, ambiguous, best_type
end

local function build_link_text(sense_index, pos_header, ambiguous, matched_type, args)
	local link_text = ""
	local is_etymology_link = ETYMOLOGY_TYPES[matched_type]

	local show_pos = args["pos"]
	if show_pos == nil then
		show_pos = ambiguous
	end

	if show_pos and pos_header and pos_header ~= "" then
		pos_header = M.string_utilities.lower(pos_header)
		link_text = pos_header .. " "
	end

	if not is_etymology_link then
		if args[3] ~= SENSE_INDEX_HIDE_LABEL then
			link_text = link_text .. SENSE_LABEL .. "&nbsp;"
		end
		link_text = link_text .. sense_index
	else
		if link_text == "" and sense_index ~= "" then
			link_text = ETYMOLOGY_LABEL .. " " .. sense_index
		end
		link_text = M.string_utilities.trim(link_text)
	end

	if args["uc"] then
		link_text = M.string_utilities.ucfirst(link_text)
	end

	return link_text
end

function export.link_text(lang_code, id, options)
	options = options or {}
	local lang = M.languages.getByCode(lang_code)
	local args = {
		[1] = lang,
		[2] = id,
		[3] = options[3],
		["title"] = options["title"],
		["pos"] = options["pos"],
		["uc"] = options["uc"],
		["t"] = options["t"],
		["q"] = options["q"],
	}
	args["link"] = (options["link"] == nil) and true or options["link"]

	local code = lang:getCode()
	local title = args["title"] or mw.title.getCurrentTitle().prefixedText
	local lang_name = lang:getCanonicalName()

	if #id == 0 then
		error("Empty or invalid id list")
	end

	local parts = {}
	for _, one_id in ipairs(id) do
		local decoded_id = mw.uri.decode(one_id, "WIKI")
		local anchor = lang_name .. ":_" .. mw.uri.anchorEncode(one_id)
		local sense_index, pos_header, ambiguous, matched_type = find_senseid(title, code, decoded_id)
		local text = build_link_text(sense_index, pos_header, ambiguous, matched_type, args)
		if args["link"] then
			text = "[[" .. title .. "#" .. anchor .. "|" .. text .. "]]"
		end
		table.insert(parts, text)
	end
	local link_text = table.concat(parts, ", ")

	if args["t"] and args["t"] ~= "" then
		link_text = link_text .. " (“" .. args["t"] .. "”)"
	end
	if args["q"] and args["q"] ~= "" then
		link_text = "(''" .. args["q"] .. "'') " .. link_text
	end
	return link_text
end

function export.link(frame)
	local boolean = { type = "boolean" }
	local params = {
		[1] = { required = true, type = "language", default = "und" },
		[2] = { required = true, sublist = "comma without whitespace" },
		[3] = true,
		["title"] = true,
		["q"] = true,
		["pos"] = boolean,
		["t"] = true,
		["uc"] = boolean,
		["cap"] = { alias_of = "uc" },
		["link"] = { type = "boolean", default = true },
	}
	local args = M.parameters.process(frame:getParent().args, params)
	return export.link_text(args[1]:getCode(), args[2], {
		[3] = args[3],
		["title"] = args["title"],
		["pos"] = args["pos"],
		["uc"] = args["uc"],
		["link"] = args["link"],
		["t"] = args["t"],
		["q"] = args["q"],
	})
end

return export