မေႃႇၵျူး:senseno
Appearance
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 .. " "
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