48,406
edits
No edit summary |
No edit summary |
||
| (One intermediate revision by the same user not shown) | |||
| Line 1: | Line 1: | ||
local export = {} | local export = {} | ||
local | local collation_module = "Module:collation" | ||
local | local headword_data_module = "Module:headword/data" | ||
local | local JSON_module = "Module:JSON" | ||
local languages_module = "Module:languages" | |||
local links_module = "Module:links" | |||
local pages_module = "Module:pages" | |||
local parameter_utilities_module = "Module:parameter utilities" | |||
local parameters_module = "Module:parameters" | |||
local parse_utilities_module = "Module:parse utilities" | |||
local pron_qualifier_module = "Module:pron qualifier" | |||
local qualifier_module = "Module:qualifier" | |||
local string_utilities_module = "Module:string utilities" | |||
local table_module = "Module:table" | |||
local utilities_module = "Module:utilities" | |||
local yesno_module = "Module:yesno" | |||
local m_str_utils = require(string_utilities_module) | |||
local function | local concat = table.concat | ||
local html = mw.html.create | |||
local is_substing = mw.isSubsting | |||
local insert = table.insert | |||
local rmatch = m_str_utils.match | |||
local | local remove = table.remove | ||
if | local sub = string.sub | ||
local trim = m_str_utils.trim | |||
local u = m_str_utils.char | |||
local dump = mw.dumpObject | |||
local function deepEquals(...) | |||
deepEquals = require(table_module).deepEquals | |||
return deepEquals(...) | |||
end | |||
local function term_already_linked(term) | |||
return term == "?" or -- signals an unknown term | |||
-- optimization to avoid unnecessarily loading [[Module:parse utilities]] | |||
(term:find("[<{]") and require(parse_utilities_module).term_already_linked(term)) | |||
end | |||
local function convert_delimiter_to_separator(item, itemind, args) | |||
if itemind == 1 then | |||
item.separator = nil | |||
elseif item.delimiter == " " then | |||
item.separator = args.space_delim | |||
elseif item.delimiter == "~" then | |||
item.separator = args.tilde_delim | |||
else | |||
item.separator = args.comma_delim | |||
end | |||
end | |||
local function get_horizontal_separator(args_horiz, embedded_comma) | |||
return args_horiz == "bullet" and " · " or embedded_comma and "; " or ", " | |||
end | |||
-- Suppress false positives in categories like [[Category:English links with redundant wikilinks]] so people won't | |||
-- be tempted to "correct" them; terms like embedded ~ like [[Micros~1]] or embedded comma not followed by a space | |||
-- such as [[1,6-Cleves acid]] need to have a link around them to avoid the tilde or comma being interpreted as a | |||
-- delimiter. | |||
local function suppress_redundant_wikilink_cat(term, alt) | |||
return term:find("~") or term:find(",%S") | |||
end | |||
local function full_link_and_track_self_links(item, face) | |||
if item.term then | |||
local pagename = mw.loadData(headword_data_module).pagename | |||
local term_is_pagename = item.term == pagename | |||
local term_contains_pagename = item.term:find("%[%[" .. m_str_utils.pattern_escape(pagename) .. "[|%]]") | |||
if term_is_pagename or term_contains_pagename then | |||
local current_L2 = require(pages_module).get_current_L2() | |||
if current_L2 then | |||
local current_L2_lang = require(languages_module).getByCanonicalName(current_L2) | |||
if current_L2_lang and current_L2_lang:getCode() == item.lang:getCode() then | |||
if term_is_pagename then | |||
track("term-is-pagename") | |||
else | |||
track("term-contains-pagename") | |||
end | |||
end | |||
end | end | ||
end | end | ||
end | end | ||
return table.concat( | item.suppress_redundant_wikilink_cat = suppress_redundant_wikilink_cat | ||
return require(links_module).full_link(item, face) | |||
end | |||
local function format_subitem(subitem, lang, face, compute_embedded_comma) | |||
local embedded_comma = false | |||
local text | |||
if subitem.term and term_already_linked(subitem.term) then | |||
text = subitem.term | |||
if compute_embedded_comma then | |||
embedded_comma = not not require(utilities_module).get_plaintext(text):find(",") | |||
end | |||
else | |||
text = full_link_and_track_self_links(subitem, face) | |||
if compute_embedded_comma then | |||
-- We don't check qualifier, label or reference text for commas as it's inside parens or displayed | |||
-- elsewhere. | |||
local subitem_plaintext = subitem.alt or subitem.term | |||
if subitem_plaintext then | |||
embedded_comma = not not subitem_plaintext:find(",") | |||
end | |||
end | |||
end | |||
-- We could use the "show qualifiers" flag to full_link() but not when term_already_linked(). | |||
if subitem.q and subitem.q[1] or subitem.qq and subitem.qq[1] or subitem.l and subitem.l[1] or | |||
subitem.ll and subitem.ll[1] or subitem.refs and subitem.refs[1] then | |||
text = require(pron_qualifier_module).format_qualifiers { | |||
lang = subitem.lang or args.lang, | |||
text = text, | |||
q = subitem.q, | |||
qq = subitem.qq, | |||
l = subitem.l, | |||
ll = subitem.ll, | |||
refs = subitem.refs, | |||
} | |||
end | |||
return text, embedded_comma | |||
end | |||
function export.format_item(item, args, face) | |||
local compute_embedded_comma = args.horiz == "comma" | |||
local embedded_comma = false | |||
if type(item) == "table" then | |||
if item.terms then | |||
local parts = {} | |||
local is_first = true | |||
for _, subitem in ipairs(item.terms) do | |||
if subitem == false then | |||
-- omitted subitem; do nothing | |||
else | |||
local separator = subitem.separator or not is_first and (args.subitem_separator or ", ") | |||
if separator then | |||
if compute_embedded_comma then | |||
embedded_comma = embedded_comma or not not separator:find(",") | |||
end | |||
insert(parts, separator) | |||
end | |||
local formatted, this_embedded_comma = format_subitem(subitem, args.lang, face, | |||
compute_embedded_comma) | |||
embedded_comma = embedded_comma or this_embedded_comma | |||
insert(parts, formatted) | |||
is_first = false | |||
end | |||
end | |||
return concat(parts), embedded_comma | |||
else | |||
return format_subitem(item, args.lang, face, compute_embedded_comma) | |||
end | |||
else | |||
if compute_embedded_comma then | |||
embedded_comma = not not require(utilities_module).get_plaintext(item):find(",") | |||
end | |||
if args.lang and not term_already_linked(item) then | |||
return full_link_and_track_self_links({lang = args.lang, term = item, sc = args.sc}, face), embedded_comma | |||
else | |||
return item, embedded_comma | |||
end | |||
end | |||
end | |||
function export.construct_old_style_header(header, horiz) | |||
local old_style_header | |||
local function ib_colon() | |||
return tostring(html("span"):addClass("ib-colon"):addClass("ib-content"):wikitext(":")) | |||
end | |||
if horiz then | |||
old_style_header = require(qualifier_module).format_qualifiers { | |||
qualifiers = header, | |||
open = false, | |||
close = false, | |||
} .. ib_colon() .. " " | |||
else | |||
old_style_header = require(qualifier_module).format_qualifiers { | |||
qualifiers = header | |||
} .. ib_colon() | |||
old_style_header = tostring(html("div"):wikitext(old_style_header)) | |||
end | |||
return old_style_header | |||
end | |||
-- Construct the sort base of a single term. As a hack, sort appendices after mainspace items. | |||
local function term_sortbase(val) | |||
if not val then | |||
-- This should not normally happen. | |||
return u(0x10FFFF) | |||
elseif val:find("^%[*Appendix:") then | |||
return u(0x10FFFE) .. val | |||
else | |||
return val | |||
end | |||
end | |||
-- Construct the sort base of a single item, using the display form preferentially, otherwise the term itself. | |||
-- As a hack, sort appendices after mainspace items. | |||
local function item_sortbase(item) | |||
return term_sortbase(item.alt or item.term) | |||
end | |||
local function make_sortbase(item) | |||
if item == false then | |||
return "*" -- doesn't matter, will be omitted in create_list() | |||
elseif type(item) == "table" then | |||
if item.terms then | |||
-- Optimize for the common case of only a single term | |||
if item.terms[2] then | |||
local parts = {} | |||
-- multiple terms | |||
local first = true | |||
for _, subitem in ipairs(item.terms) do | |||
if subitem ~= false then | |||
if not first then | |||
insert(parts, ", ") | |||
end | |||
insert(parts, item_sortbase(subitem)) | |||
first = false | |||
end | |||
end | |||
if parts[1] then | |||
return concat(parts) | |||
end | |||
else | |||
local subitem = item.terms[1] | |||
if subitem ~= false then | |||
return item_sortbase(subitem) | |||
end | |||
end | |||
return "*" -- doesn't matter, entire group will be omitted in create_list() | |||
else | |||
return item_sortbase(item) | |||
end | |||
else | |||
return item | |||
end | |||
end | |||
local function make_node_sortbase(node) | |||
return make_sortbase(node.item) | |||
end | |||
-- Sort a sublist of `list` in place, keeping the first `keepfirst` and last `keeplast` items fixed. | |||
-- `lang` is the language of the items and `make_sortbase` creates the appropriate sort base. | |||
local function sort_sublist(list, lang, make_sortbase, keepfirst, keeplast) | |||
if keepfirst == 0 and keeplast == 0 then | |||
require(collation_module).sort(list, lang, make_sortbase) | |||
else | |||
local sublist = {} | |||
for i = keepfirst + 1, #list - keeplast do | |||
sublist[i - keepfirst] = list[i] | |||
end | |||
require(collation_module).sort(sublist, lang, make_sortbase) | |||
for i = keepfirst + 1, #list - keeplast do | |||
list[i] = sublist[i - keepfirst] | |||
end | |||
end | |||
end | |||
-- URL-encode only the characters that serve as template delimiters (left and right brace, vertical bar, equal sign | |||
-- and percent sign since it's the escape character). | |||
local function bot_url_encode(txt) | |||
return (txt:gsub("[%%|{}=&]", | |||
{["%"] = "%25", ["|"] = "%7C", ["{"] = "%7B", ["}"] = "%7D", ["="] = "%3D", ["&"] = "%26"})) | |||
end | |||
-- Reverse the action of bot_url_encode(). | |||
local function bot_url_decode(txt) | |||
return (txt:gsub("%%7([BCD])", {B = "{", C = "|", D = "}"}):gsub("%%3D", "="):gsub("%%26", "&"):gsub("%%25", "%%")) | |||
end | |||
--[==[ | |||
Bot-callable function to generate a number of sortkeys simultaneously. {{para|1}} contains the langcode, and remaining | |||
numeric parameters contain "bot-URL-encoded" strings whose sort keys will be computed and returned as a JSON array. | |||
Here, "bot-URL-encoded" means that the six characters `{ | } = & %` should be converted to | |||
their URL-encoded representation (respectively <code>%7B %7C %7D %3D %26 %25</code>), and will be decoded appropriately | |||
before computing the sortkey. | |||
]==] | |||
function export.make_sortkey(frame) | |||
local iparams = { | |||
[1] = {type = "language"}, | |||
[2] = {list = true}, | |||
} | |||
local iargs = require(parameters_module).process(frame.args, iparams) | |||
local make_sortkey = require(collation_module).make_lang_sortkey_function(iargs[1], term_sortbase) | |||
local retval = {} | |||
for _, arg in ipairs(iargs[2]) do | |||
arg = bot_url_decode(arg) | |||
insert(retval, make_sortkey(arg)) | |||
end | |||
return require(JSON_module).toJSON(retval) | |||
end | end | ||
local large_text_scripts = { | |||
["Arab"] = true, | |||
["Beng"] = true, | |||
["Deva"] = true, | |||
["Gujr"] = true, | |||
["Guru"] = true, | |||
["Hebr"] = true, | |||
["Khmr"] = true, | |||
["Knda"] = true, | |||
["Laoo"] = true, | |||
["Mlym"] = true, | |||
["Mong"] = true, | |||
["Mymr"] = true, | |||
["Orya"] = true, | |||
["Sinh"] = true, | |||
["Syrc"] = true, | |||
["Taml"] = true, | |||
["Telu"] = true, | |||
["Tfng"] = true, | |||
["Thai"] = true, | |||
["Tibt"] = true, | |||
} | |||
--[==[ | |||
Format a list of items using HTML. `args` is an object specifying the items to add and related properties, with the | |||
following fields: | |||
* `content`: A list of the items to format. See below for the format of the items. | |||
* `lang`: The language object of the items to format, if the items in `content` are strings. | |||
* `sc`: The script object of the items to format, if the items in `content` are strings. | |||
* `raw`: If true, return the list raw, without any collapsing or columns. | |||
* `class`: The CSS class of the surrounding <div>. | |||
* `column_count`: Number of columns to format the list into. | |||
* `alphabetize`: If true, sort the items in the table. | |||
* `collapse`: If true, make the table partially collapsed by default, with a "Show more" button at the bottom. | |||
* `toggle_category`: Value of `data-toggle-category` property grouping collapsible elements. | |||
* `header`: If specified, Wikicode to prepend to the output. | |||
* `title_new_style`: If true, the header is treated as a title and displayed in a new style. This is ignored if `horiz` | |||
is non-nil. | |||
* `subitem_separator`: Separator used between subitems when multiple subitems occur on a line, if not specified in the | |||
subitem itself (using the `separator` field). Defaults to {", "}. | |||
* `keepfirst`: If > 0, keep this many rows unsorted at the beginning of the top level. | |||
* `keeplast`: If > 0, keep this many rows unsorted at the end of the top level. | |||
* `horiz`: If non-nil, format the items horizontally. If the value is "bullet", put a center dot/bullet (·) between | |||
items. If the value is "comma", put a comma between items (but if there is an embedded comma in any item, | |||
put a semicolon between all items). | |||
Each item in `content` is in one of the following formats: | |||
* A string. This is for compatibility and should not be used by new callers. | |||
* An object describing an item to format, in the format expected by full_link() in [[Module:links]] but can also | |||
have left or right qualifiers, left or right labels, or references. | |||
* An object describing a list of subitems to format, displayed side-by-side, separated by a comma or other separator. | |||
This format is identified by the presence of a key `terms` specifying the list of subitems. Each subitem is in | |||
the same format as for a single top-level item, except that it should also have a `separator` field specifying the | |||
separator to display before each item (which will typically be a blank string before the first item). | |||
]==] | |||
function export.create_list(args) | function export.create_list(args) | ||
if type(args) ~= "table" then | if type(args) ~= "table" then | ||
error("expected table, got " .. type(args)) | error("expected table, got " .. type(args)) | ||
end | end | ||
args. | local column_count = args.column_count or 1 | ||
local toggle_category = args.toggle_category or "derived terms" | |||
local keepfirst = args.keepfirst or 0 | |||
local keeplast = args.keeplast or 0 | |||
local | if keepfirst > 0 then | ||
track("keepfirst") | |||
if args.header then | end | ||
if args. | if keeplast > 0 then | ||
track("keeplast") | |||
end | |||
-- maybe construct old-style header | |||
local old_style_header = nil | |||
if args.header and (args.horiz or not args.title_new_style) then | |||
old_style_header = export.construct_old_style_header(args.header, args.horiz) | |||
end | |||
if args.horiz then | |||
old_style_header = "* " .. (old_style_header or "") | |||
end | |||
local list | |||
local any_extra_indented_item = false | |||
for _, item in ipairs(args.content) do | |||
if item == false then | |||
-- do nothing | |||
elseif type(item) == "table" and item.extra_indent and item.extra_indent > 0 then | |||
any_extra_indented_item = true | |||
break | |||
end | end | ||
end | end | ||
if args. | |||
-- If any extra indented item, convert the items to a nested structure, which is necessary both for sorting and | |||
-- for converting to HTML. | |||
if any_extra_indented_item then | |||
local function make_node(item) | |||
return { | |||
item = item | |||
} | |||
end | |||
local root_node = make_node(nil) | |||
local node_stack = {root_node} | |||
local last_indent = 0 | |||
local function append_subnode(node, subnode) | |||
if not node.subnodes then | |||
node.subnodes = {} | |||
end | |||
insert(node.subnodes, subnode) | |||
end | |||
for i, item in ipairs(args.content) do | |||
if item == false then | |||
-- do nothing | |||
else | |||
local this_indent | |||
if type(item) ~= "table" then | |||
this_indent = 1 | |||
else | |||
this_indent = (item.extra_indent or 0) + 1 | |||
end | |||
local node = make_node(item) | |||
if this_indent == last_indent then | |||
append_subnode(node_stack[#node_stack], node) | |||
elseif this_indent > last_indent + 1 then | |||
error(("Element #%s (%s) has indent %s, which is more than one greater than the previous item with indent %s"):format( | |||
i, make_sortbase(item), this_indent, last_indent)) | |||
elseif this_indent > last_indent then | |||
-- Start a new sublist attached to the last item of the sublist one level up; but we need special | |||
-- handling for the root node (last_indent == 0). | |||
if last_indent > 0 then | |||
local subnodes = node_stack[#node_stack].subnodes | |||
if not subnodes then | |||
error(("Internal error: Not first item and no subnodes at preceding level %s: %s"):format( | |||
#node_stack, dump(node_stack))) | |||
end | |||
insert(node_stack, subnodes[#subnodes]) | |||
end | |||
append_subnode(node_stack[#node_stack], node) | |||
last_indent = this_indent | |||
else | |||
while last_indent > this_indent do | |||
local finished_node = table.remove(node_stack) | |||
if args.alphabetize then | |||
require(collation_module).sort(finished_node.subnodes, args.lang, make_node_sortbase) | |||
end | |||
last_indent = last_indent - 1 | |||
end | |||
append_subnode(node_stack[#node_stack], node) | |||
end | |||
end | |||
end | |||
if args.alphabetize then | |||
while node_stack[1] do | |||
local finished_node = table.remove(node_stack) | |||
if node_stack[1] then | |||
-- We're sorting something other than the root node. | |||
require(collation_module).sort(finished_node.subnodes, args.lang, make_node_sortbase) | |||
else | |||
-- We're sorting the root node; honor `keepfirst` and `keeplast`. | |||
sort_sublist(finished_node.subnodes, args.lang, make_node_sortbase, keepfirst, keeplast) | |||
end | |||
end | |||
end | |||
local function format_node(node, depth) | |||
local sublist | |||
local embedded_comma = false | |||
if node.subnodes then | |||
if args.horiz then | |||
sublist = {} | |||
else | |||
sublist = html("ul") | |||
end | |||
local prevnode = nil | |||
for _, subnode in ipairs(node.subnodes) do | |||
local thisnode, this_embedded_comma = format_node(subnode, depth + 1) | |||
embedded_comma = embedded_comma or this_embedded_comma | |||
if not prevnode or not args.alphabetize or not deepEquals(prevnode, thisnode) then | |||
if args.horiz then | |||
table.insert(sublist, thisnode) | |||
else | |||
sublist = sublist:node(thisnode) | |||
end | |||
prevnode = thisnode | |||
end | |||
end | |||
if args.horiz then | |||
sublist = table.concat(sublist, get_horizontal_separator(args.horiz, embedded_comma)) | |||
end | |||
end | |||
if not node.item then | |||
-- At the root. | |||
return sublist, embedded_comma | |||
end | |||
local formatted, listitem | |||
-- Ignore embedded commas in subitems inside of parens or square brackets. | |||
formatted, embedded_comma = export.format_item(node.item, args) | |||
if args.horiz then | |||
listitem = formatted | |||
if sublist then | |||
-- Use parens for the first, third, fifth, etc. sublists and square brackets for the remainder. | |||
if depth % 2 == 1 then | |||
listitem = ("%s (%s)"):format(listitem, sublist) | |||
else | |||
listitem = ("%s [%s]"):format(listitem, sublist) | |||
end | |||
end | |||
else | |||
listitem = html("li"):wikitext(formatted) | |||
if sublist then | |||
listitem = listitem:node(sublist) | |||
end | |||
end | |||
return listitem, embedded_comma | |||
end | |||
list = format_node(root_node, 0) | |||
else | |||
if args.alphabetize then | |||
sort_sublist(args.content, args.lang, make_sortbase, keepfirst, keeplast) | |||
end | |||
if args.horiz then | |||
list = {} | |||
else | |||
list = html("ul") | |||
end | |||
local previtem = nil | |||
local embedded_comma = false | |||
for _, item in ipairs(args.content) do | |||
if item == false then | |||
-- omitted item; do nothing | |||
else | |||
local thisitem, this_embedded_comma = export.format_item(item, args) | |||
embedded_comma = embedded_comma or this_embedded_comma | |||
if not previtem or not args.alphabetize or previtem ~= thisitem then | |||
if args.horiz then | |||
table.insert(list, thisitem) | |||
else | |||
list = list:node(html("li"):wikitext(thisitem)) | |||
end | |||
previtem = thisitem | |||
end | |||
end | |||
end | |||
if args.horiz then | |||
list = table.concat(list, get_horizontal_separator(args.horiz, embedded_comma)) | |||
end | |||
end | end | ||
local output | |||
if args.horiz then | |||
output = list | |||
else | |||
output = html("div"):addClass("term-list"):node(list) | |||
if args.class then | |||
output:addClass(args.class) | |||
end | |||
if not args.raw then | |||
output:addClass("ul-column-count") | |||
:attr("data-column-count", column_count) | |||
if args.collapse then | |||
output = html("div") | |||
:node(output) | |||
:addClass("list-switcher") | |||
:attr("data-toggle-category", toggle_category) | |||
-- identify commonly used scripts that use large text and | |||
-- provide a special CSS class to make the template bigger | |||
local sc = args.sc | |||
if sc == nil then | |||
local scripts = args.lang:getScripts() | |||
if #scripts > 0 then | |||
sc = scripts[1] | |||
end | |||
end | |||
if sc ~= nil then | |||
local scriptcode = sc:getParentCode() | |||
if scriptcode == "top" then | |||
scriptcode = sc:getCode() | |||
end | |||
if large_text_scripts[scriptcode] then | |||
output:addClass("list-switcher-large-text") | |||
end | |||
end | |||
end | |||
end | |||
if args.collapse or args.title_new_style then | |||
-- wrap in wrapper to prevent interference from floating elements | |||
local list_switcher_wrapper = html("div") | |||
:addClass("list-switcher-wrapper") | |||
if args.title_new_style then | |||
list_switcher_wrapper | |||
:node( | |||
html("div") | |||
:addClass("list-switcher-header") | |||
:wikitext(args.header) | |||
) | |||
end | |||
list_switcher_wrapper:node(output) | |||
output = list_switcher_wrapper | |||
end | |||
output = tostring(output) | |||
end | end | ||
return (old_style_header or "") .. output | |||
end | end | ||
| Line 90: | Line 617: | ||
-- n_columns, content, alphabetize, bg, collapse, class, title, column_width, line_start, lang | -- n_columns, content, alphabetize, bg, collapse, class, title, column_width, line_start, lang | ||
local args = {} | local args = {} | ||
args.column_count, args.content, args.alphabetize | args.column_count, args.content, args.alphabetize, | ||
args.collapse, args.class, args.header, args.column_width, | args.collapse, args.class, args.header, args.column_width, | ||
args.line_start, args.lang = ... | args.line_start, args.lang = ... | ||
return export.create_list(args) | return export.create_list(args) | ||
end | end | ||
local | function export.display_from(frame_args, parent_args, frame) | ||
local boolean = {type = "boolean"} | |||
local iparams = { | local iparams = { | ||
["class"] = | ["class"] = true, | ||
-- Default for auto-collapse. Overridable by template |collapse= param. | -- Default for auto-collapse. Overridable by template |collapse= param. | ||
["collapse"] = | ["collapse"] = boolean, | ||
-- If specified, this specifies the number of columns, and no columns | -- If specified, this specifies the number of columns, and no columns parameter is available on the template. | ||
-- Otherwise, the columns parameter is named |n=. | |||
["columns"] = {type = "number"}, | ["columns"] = {type = "number"}, | ||
-- If specified, this specifies the language code, | -- If specified, this specifies the default language code, which can be overridden using |lang= in the template. | ||
-- | -- Otherwise, the language-code parameter is required and normally found in |1=, but for compatibility can be | ||
-- | -- specified as |lang= (which leads to deprecation handling). | ||
["lang"] = {}, | ["lang"] = {type = "language"}, | ||
-- Default for auto-sort. Overridable by template |sort= param. | -- Default for auto-sort. Overridable by template |sort= param. | ||
["sort"] = | ["sort"] = boolean, | ||
-- | ["toggle_category"] = true, | ||
-- | -- Minimum number of rows required to format into a multicolumn list. If below this, the list is displayed "raw" | ||
[" | -- (no columns, no collapsbility). | ||
["minrows"] = {type = "number", default = 5}, | |||
} | } | ||
local | local iargs = require(parameters_module).process(frame_args, iparams) | ||
local parent_args = frame: | |||
local langcode_in_lang = iargs.lang or parent_args.lang | |||
local | local lang_param = langcode_in_lang and "lang" or 1 | ||
local lang_param = | local deprecated = not iargs.lang and langcode_in_lang | ||
local | |||
local ret = export.handle_display_from_or_topic_list(iargs, parent_args, nil) | |||
return deprecated and frame:expandTemplate{title = "check deprecated lang param usage", | |||
args = {ret, lang = args[lang_param]}} or ret | |||
end | |||
--[==[ | |||
Implement `display_from()` [the internal entry point for {{tl|col}} and variants, which enter originally through | |||
`display()`] as well as regular (column-oriented) topic lists, invoked through [[Module:topic list]]. | |||
`iargs` are the invocation args of {{tl|col}}, and `raw_item_args` are the arguments specifying the values of | |||
each row as well as other properties, corresponding to the user-specified template arguments of {{tl|col}}. Note that | |||
`show()` in [[Module:topic list]] is normally invoked directly by a topic list template, whose invocation | |||
arguments are passed in using `raw_item_args` and are similar to the template arguments of {{tl|col}}. `iargs` for | |||
topic-list invocations is hard-coded, and template arguments to a topic-list template are processed in | |||
[[Module:topic list]] itself. Note that the handling of topic lists is currently implemented almost entirely | |||
through callbacks in `topic_list_data` (which is nil if we're processing {{tl|col}} rather than a topic list) in an | |||
attempt to reduce the coupling and keep the topic-list-specific code in [[Module:topic list]], but IMO the coupling | |||
is still too tight. Probably the control structure should be reversed and the following function split up into | |||
subfunctions, which are invoked as needed by {{tl|col}} and/or [[Module:topic list]]. | |||
]==] | |||
function export.handle_display_from_or_topic_list(iargs, raw_item_args, topic_list_data) | |||
local boolean = {type = "boolean"} | |||
local langcode_in_lang = iargs.lang or raw_item_args.lang | |||
local lang_param = langcode_in_lang and "lang" or 1 | |||
local first_content_param = langcode_in_lang and 1 or 2 | |||
local params = { | local params = { | ||
[lang_param] = not | [lang_param] = {required = not iargs.lang, type = "language", | ||
[ | template_default = not iargs.lang and "und" or nil}, | ||
[first_content_param] = {list = true}, | ["n"] = not iargs.columns and {type = "number"} or nil, | ||
[first_content_param] = {list = true, allow_holes = true}, | |||
["title"] = {}, | ["title"] = {}, | ||
["collapse"] = {type = " | ["collapse"] = boolean, | ||
[" | ["sort"] = boolean, | ||
[" | ["sc"] = {type = "script"}, | ||
-- used when calling from [[Module:saurus]] so the page displaying the synonyms/antonyms doesn't occur in the | |||
-- list | |||
["omit"] = {list = true}, | |||
["keepfirst"] = {type = "number", default = 0}, | |||
["keeplast"] = {type = "number", default = 0}, | |||
["horiz"] = {}, | |||
["notr"] = boolean, | |||
["allow_space_delim"] = boolean, | |||
["tilde_delim"] = {}, | |||
["space_delim"] = {}, | |||
["comma_delim"] = {}, | |||
} | } | ||
local | if topic_list_data then | ||
topic_list_data.add_topic_list_params(params) | |||
local | end | ||
local m_param_utils = require(parameter_utilities_module) | |||
local | |||
if | local param_mods = m_param_utils.construct_param_mods { | ||
{default = true, require_index = true}, | |||
{group = "link"}, -- sc has separate_no_index = true; that's the only one | |||
-- It makes no sense to have overall l=, ll=, q= or qq= params for columnar display. | |||
{group = {"ref", "l", "q"}, require_index = true}, | |||
} | |||
m_param_utils.augment_params_with_modifiers(params, param_mods) | |||
local processed_args = require(parameters_module).process(raw_item_args, params) | |||
local horiz = processed_args.horiz | |||
if horiz and horiz ~= "comma" and horiz ~= "bullet" then | |||
horiz = require(yesno_module)(horiz) | |||
if horiz == nil then | |||
error(("Unrecognized value |horiz=%s; should be 'comma', 'bullet' or a recognized Boolean value such " .. | |||
"as 'yes' or '1' (same as 'bullet') or 'no' or '0'"):format(processed_args.horiz)) | |||
end | |||
if horiz == true then | |||
horiz = "bullet" | |||
end | |||
processed_args.horiz = horiz | |||
end | |||
-- If default argument values specified, set them after parsing the caller-specified arguments in `raw_item_args`. | |||
if topic_list_data then | |||
topic_list_data.set_default_arguments(processed_args) | |||
end | |||
-- Now set defaults for the various delimiters, depending in some cases on whether horiz was set. | |||
-- We can't set these defaults (even regardless of their dependency on horiz=) in `local params` above | |||
-- because we want any defaults specified in `default_props` to override these. | |||
if not processed_args.tilde_delim then | |||
local tilde_with_abbr = '<abbr title="near equivalent">~</abbr>' | |||
processed_args.tilde_delim = processed_args.horiz and tilde_with_abbr or " " .. tilde_with_abbr .. " " | |||
end | end | ||
if not processed_args.space_delim then | |||
processed_args.space_delim = " " | |||
if | |||
end | end | ||
if not processed_args.comma_delim then | |||
if | processed_args.comma_delim = processed_args.horiz and "/" or ", " | ||
end | end | ||
-- Check for extra term indent. Do this before calling parse_list_with_inline_modifiers_and_separate_params() | |||
-- because sometimes space is a delimiter and the space in the indent will confuse things and get interpreted as a | |||
-- delimiter. | |||
local extra_indent_by_termno = {} | |||
local termargs = processed_args[first_content_param] | |||
for i = 1, termargs.maxindex do | |||
local term = termargs[i] | |||
if term then | |||
if | local extra_indent, actual_term = rmatch(term, "^(%*+)%s+(.-)$") | ||
if | if extra_indent then | ||
termargs[i] = actual_term | |||
extra_indent_by_termno[i] = #extra_indent | |||
end | end | ||
end | |||
end | |||
local groups, args = m_param_utils.parse_list_with_inline_modifiers_and_separate_params { | |||
param_mods = param_mods, | |||
processed_args = processed_args, | |||
termarg = first_content_param, | |||
parse_lang_prefix = true, | |||
allow_multiple_lang_prefixes = true, | |||
disallow_custom_separators = true, | |||
track_module = "columns", | |||
lang = iargs.lang or lang_param, | |||
sc = "sc.default", | |||
splitchar = processed_args.allow_space_delim and "[,~ ]" or "[,~]", | |||
} | |||
local lang = iargs.lang or args[lang_param] | |||
local langcode = lang:getCode() | |||
local fulllangcode = lang:getFullCode() | |||
local sc = args.sc.default | |||
local sort = iargs.sort | |||
if args.sort ~= nil then | |||
if not args.sort then | |||
track("nosort") | |||
end | |||
sort = args.sort | |||
else | |||
-- HACK! For Japanese-script languages (Japanese, Okinawan, Miyako, etc.), sorting doesn't yet work properly, so | |||
-- disable it. | |||
for _, langsc in ipairs(lang:getScriptCodes()) do | |||
if langsc == "Jpan" then | |||
sort = false | |||
break | |||
end | |||
end | |||
end | |||
local collapse = iargs.collapse | |||
if args.collapse ~= nil then | |||
if not args.collapse then | |||
track("nocollapse") | |||
end | |||
collapse = args.collapse | |||
end | |||
local title = args.title | |||
local formatted_cats | |||
if topic_list_data then | |||
title, formatted_cats = topic_list_data.get_title_and_formatted_cats(args, lang, sc, topic_list_data) | |||
end | |||
local number_of_groups = 0 | |||
for i, group in ipairs(groups) do | |||
local number_of_items = 0 | |||
group.extra_indent = extra_indent_by_termno[group.orig_index] | |||
for j, item in ipairs(group.terms) do | |||
convert_delimiter_to_separator(item, j, args) | |||
if args.notr then | |||
item.tr = "-" | |||
end | end | ||
for | -- If a separate language code was given for the term, display the language name as a right qualifier. | ||
-- (Briefly we made them labels but this leads to non-obvious behavior e.g. "French" becoming "France" under | |||
-- some circumstances.) Otherwise it may not be obvious that the term is in a separate language (e.g. if the | |||
-- main language is 'zh' and the term language is a Chinese lect such as Min Nan). But don't do this for | |||
-- Translingual terms, which are often added to the list of English and other-language terms. | |||
if item.termlangs then | |||
local qqs = {} | |||
for _, termlang in ipairs(item.termlangs) do | |||
local termlangcode = termlang:getCode() | |||
if termlangcode ~= langcode and termlangcode ~= "mul" then | |||
insert(qqs, termlang:getCanonicalName()) | |||
end | |||
end | end | ||
if item.qq then | |||
for _, qq in ipairs(item.qq) do | |||
insert(qqs, qq) | |||
end | |||
end | end | ||
item.qq = qqs | |||
end | |||
local omitted = false | |||
for _, omitted_item in ipairs(args.omit) do | |||
if omitted_item == item.term then | |||
omitted = true | |||
break | |||
end | end | ||
end | end | ||
if omitted then | |||
-- signal create_list() to omit this item | |||
group.terms[j] = false | |||
else | |||
number_of_items = number_of_items + 1 | |||
end | |||
end | |||
if number_of_items == 0 then | |||
-- omit the whole group | |||
groups[i] = false | |||
else | |||
number_of_groups = number_of_groups + 1 | |||
end | end | ||
end | end | ||
return export.create_list { column_count = | local column_count = iargs.columns or args.n | ||
content = | -- FIXME: This needs a total rewrite. | ||
if column_count == nil then | |||
column_count = number_of_groups <= 3 and 1 or | |||
number_of_groups <= 9 and 2 or | |||
number_of_groups <= 27 and 3 or | |||
number_of_groups <= 81 and 4 or | |||
5 | |||
end | |||
local raw = number_of_groups < iargs.minrows | |||
local horiz_edit_button | |||
if topic_list_data and args.horiz then | |||
-- append edit button to title | |||
horiz_edit_button = topic_list_data.make_horiz_edit_button(topic_list_data.topic_list_template) | |||
end | |||
return export.create_list { | |||
column_count = column_count, | |||
raw = raw, | |||
content = groups, | |||
alphabetize = sort, | alphabetize = sort, | ||
header = | header = title, | ||
title_new_style = (title ~= nil and title ~= ''), | |||
collapse = collapse, | collapse = collapse, | ||
toggle_category = | toggle_category = iargs.toggle_category, | ||
class = | -- columns-bg (in [[MediaWiki:Gadget-Site.css]]) provides the background color | ||
class = (iargs.class and iargs.class .. " columns-bg" or "columns-bg"), | |||
lang = lang, | |||
sc = sc, | |||
subitem_separator = ", ", | |||
keepfirst = args.keepfirst, | |||
keeplast = args.keeplast, | |||
horiz = args.horiz, | |||
} .. (horiz_edit_button or "") .. (formatted_cats or "") | |||
end | end | ||
function export.display(frame) | |||
if not is_substing() then | |||
return export.display_from(frame.args, frame:getParent().args, frame, false) | |||
end | |||
-- If substed, unsubst template with newlines between each term, redundant wikilinks removed, and remove duplicates + sort terms if sort is enabled. | |||
local m_table = require("Module:table") | |||
local m_template_parser = require("Module:template parser") | |||
local parent = frame:getParent() | |||
local elems = m_table.shallowCopy(parent.args) | |||
local code = remove(elems, 1) | |||
code = code and trim(code) | |||
local lang = require("Module:languages").getByCode(code, 1) | |||
local i = 1 | |||
while true do | |||
local elem = elems[i] | |||
while elem do | |||
elem = trim(elem, "%s") | |||
if elem ~= "" then | |||
break | |||
end | |||
remove(elems, i) | |||
elem = elems[i] | |||
end | |||
if not elem then | |||
break | |||
elseif not ( -- Strip redundant wikilinks. | |||
not elem:match("^()%[%[") or | |||
elem:find("[[", 3, true) or | |||
elem:find("]]", 3, true) ~= #elem - 1 or | |||
elem:find("|", 3, true) | |||
) then | |||
elem = sub(elem, 3, -3) | |||
elem = trim(elem, "%s") | |||
end | |||
elems[i] = elem .. "\n" | |||
i = i + 1 | |||
end | |||
-- If sort is enabled, remove duplicates then sort elements. | |||
if require("Module:yesno")(frame.args.sort) then | |||
elems = m_table.removeDuplicates(elems) | |||
require("Module:collation").sort(elems, lang) | |||
end | |||
-- Readd the langcode. | |||
insert(elems, 1, code .. "\n") | |||
-- TODO: Place non-numbered parameters after 1 and before 2. | |||
local template = m_template_parser.getTemplateInvocationName(mw.title.new(parent:getTitle())) | |||
return "{{" .. concat(m_template_parser.buildTemplate(template, elems), "|") .. "}}" | |||
end | |||
return export | return export | ||