Mô đun:Cs1 documentation support

require('strict');
local getArgs = require ('Module:Arguments').getArgs;

local cfg = mw.loadData ('Module:Citation/CS1/Configuration'); -- load the configuration module
local whitelist = mw.loadData ('Module:Citation/CS1/Whitelist'); -- load the whitelist module

local exclusion_lists = { -- TODO: move these tables into a separate ~/data module and mw.loadData() it
['cite book'] = {
['agency'] = true,
['air-date'] = true,
['arxiv'] = true,
['biorxiv'] = true,
['citeseerx'] = true,
['class'] = true,
['conference'] = true,
['conference-format'] = true,
['conference-url'] = true,
['degree'] = true,
['department'] = true,
['display-interviewers'] = true,
['docket'] = true,
['episode'] = true,
['interviewer'] = true,
['interviewer-first'] = true,
['interviewer-link'] = true,
['interviewer-mask'] = true,
['ismn'] = true,
['issn'] = true,
['issue'] = true,
['jfm'] = true,
['journal'] = true,
['jstor'] = true,
['mailinglist'] = true,
['message-id'] = true,
['minutes'] = true,
['MR'] = true,
['network'] = true,
['number'] = true,
['RFC'] = true,
['script-journal'] = true,
['season'] = true,
['section'] = true,
['sections'] = true,
['series-link'] = true,
['series-number'] = true,
['series-separator'] = true,
['sheet'] = true,
['sheets'] = true,
['SSRN'] = true,
['station'] = true,
['time'] = true,
['time-caption'] = true,
['trans-article'] = true,
['trans-journal'] = true,
['transcript'] = true,
['transcript-format'] = true,
['transcript-url'] = true,
['ZBL'] = true,
},
['cite journal'] = {
['agency'] = true,
['air-date'] = true,
['book-title'] = true,
['chapter'] = true,
['chapter-format'] = true,
['chapter-url'] = true,
['chapter-url-access'] = true,
['class'] = true,
['conference'] = true,
['conference-format'] = true,
['conference-url'] = true,
['contribution'] = true,
['contributor'] = true,
['contributor-first'] = true,
['contributor-link'] = true,
['contributor-mask'] = true,
['degree'] = true,
['department'] = true,
['display-interviewers'] = true,
['docket'] = true,
['edition'] = true,
['editor'] = true,
['editor-first'] = true,
['editor-link'] = true,
['editor-mask'] = true,
['editors'] = true,
['encyclopedia'] = true,
['episode'] = true,
['ignore-isbn-error'] = true,
['interviewer'] = true,
['interviewer-first'] = true,
['interviewer-link'] = true,
['interviewer-mask'] = true,
['isbn'] = true,
['ismn'] = true,
['LCCN'] = true,
['mailinglist'] = true,
['message-id'] = true,
['minutes'] = true,
['network'] = true,
['script-chapter'] = true,
['season'] = true,
['section'] = true,
['sections'] = true,
['series-link'] = true,
['series-number'] = true,
['series-separator'] = true,
['sheet'] = true,
['sheets'] = true,
['station'] = true,
['time'] = true,
['time-caption'] = true,
['trans-article'] = true,
['transcript'] = true,
['transcript-format'] = true,
['transcript-url'] = true,
},
}

--script-= lang codes always use override names so dagger is omitted

local function add_to_list (code_list, name_list, override_list, code, name, dagger)
if false == dagger then
dagger = ; -- no dagger for |script-= codes and names
else
dagger = '†'; -- dagger for all other lists using override
end

if override_list[code] then -- look in the override table for this code
code_list[code] = override_list[code] .. dagger; -- use the name from the override table; mark with dagger
name_list[override_list[code]] = code .. dagger;
else
code_list[code] = name; -- use the MediaWiki name and code
name_list[name] = code;
end
end

--[[----------------------------------------------------------------------------------

formats key/value pair into a string for rendering
['k'] = 'v' → k: v

]]

local function list_format (result, list)
for k, v in pairs (list) do
table.insert (result, k .. ': ' .. v);
end
end

------------------------------------------------------------------------------------

Module entry point

Crude documentation tool that returns one of several lists of language codes and names.

Used in Template:Citation Style documentation/language/doc

where is one of the values:
2char – list of ISO 639-1 codes and names sorted by code
3char – list of ISO 639-2, -3 codes and names sorted by code
ietf – list of IETF language tags and names sorted by tag
ietf2 – list of ISO 639-1 based IETF language tags and names sorted by tag
ietf3 – list of list of ISO 639-2, -3 based IETF language tags and names sorted by tag
name – list of language names and codes sorted by name
all - list all language codes/tags and names sorted by code/tag

where is a MediaWiki supported 2, 3, or ietf-like language code; because of fall-back, language names may
be the English-language names.

local function lang_lister (frame)
local lang = (frame.args.lang and ~= frame.args.lang) and frame.args.lang or mw.getContentLanguage():getCode()
local source_list = mw.language.fetchLanguageNames(lang, 'all');
local override = cfg.lang_tag_remap;
local code_1_list={};
local code_2_list={};
local ietf_list={};
local ietf_list2={};
local ietf_list3={};
local name_list={};

if not ({['2char']=true, ['3char']=true, ['ietf']=true, ['ietf2']=true, ['ietf3']=true, ['name']=true, ['all']=true})[frame.args.list] then
return 'unknown list selector: ' .. frame.args.list .. ;
end

for code, name in pairs (source_list) do
if 'all' frame.args.list then
add_to_list (code_1_list, name_list, override, code, name); -- use the code_1_list because why not?
elseif 2 code:len() then
add_to_list (code_1_list, name_list, override, code, name);
elseif 3 == code:len() then
add_to_list (code_2_list, name_list, override, code, name);
elseif code:match ('^%a%a%-.+') then -- ietf with 2-character language tag
add_to_list (ietf_list, name_list, override, code, name); -- add to main ietf list for |list=ietf
add_to_list (ietf_list2, name_list, override, code, name); -- add to ietf2 list
elseif code:match ('^%a%a%a%-.+') then -- ietf with 3-character language tag
add_to_list (ietf_list, name_list, override, code, name); -- add to main ietf list for |list=ietf
add_to_list (ietf_list3, name_list, override, code, name); -- add to ietf3 list
end
end

local result = {};
local out = {};

if '2char' frame.args.list or 'all' frame.args.list then -- iso 639-1
list_format (result, code_1_list);
elseif '3char' frame.args.list then -- iso 639-2, 3
list_format (result, code_2_list);
elseif 'ietf' frame.args.list then -- all ietf tags
list_format (result, ietf_list);
elseif 'ietf2' frame.args.list then -- 2-character ietf tags
list_format (result, ietf_list2);
elseif 'ietf3' frame.args.list then -- 3 character ietf tags
list_format (result, ietf_list3);
else --must be 'name'
list_format (result, name_list);
end

local templatestyles = frame:extensionTag{
name = 'templatestyles', args = { src = "Div col/styles.css" }
}

table.sort (result);
table.insert (result, 1, templatestyles .. );
table.insert (out, table.concat (result, '\n'));
table.insert (out, );

return table.concat (out, '\n');
end

--script-= parameters.

used in Help:CS1 errors

local function script_lang_lister (frame)
local lang_code_src = cfg.script_lang_codes ; -- get list of allowed script language codes
local override = cfg.lang_tag_remap;
local this_wiki_lang = mw.language.getContentLanguage().code; -- get this wiki's language

local code_list = {}; -- interim list of aliases
local name_list={}; -- not used; defined here so that we can reuse add_to_list()
local out = {}; -- final output (for now an unordered list)

for _, code in ipairs (lang_code_src) do -- loop through the list of codes
local name = mw.language.fetchLanguageName (code, this_wiki_lang); -- get the language name associated with this code
add_to_list (code_list, name_list, override, code, name, false); -- name_list{} not used but provided so that we can reuse add_to_list(); don't add superscript dagger
end

local result = {};
local out = {};

list_format (result, code_list);

local templatestyles = frame:extensionTag{
name = 'templatestyles', args = { src = "Div col/styles.css" }
}

table.sort (result);
table.insert (result, 1, templatestyles .. );
table.insert (out, table.concat (result, '\n'));
table.insert (out, );

return table.concat (out, '\n');
end

----------------------------------------------------------------------------------

experimental code that lists parameters and their aliases. Perhaps basis for some sort of documentation?

local function alias_lister ()
local alias_src = cfg.aliases; -- get master list of aliases
local key; -- key for k/v in a new table
local list = {}; -- interim list of aliases
local out = {}; -- final output (for now an unordered list)

for _, aliases in pairs (alias_src) do -- loop throu the master list of aliases
if 'table' type (aliases) then -- table only when there are aliases
for i, alias in ipairs (aliases) do -- loop through all of the aliases
if 1 i then -- first 'alias' is the canonical parameter name
key = alias; -- so it becomes the key in list
else
list[key] = list[key] and (list[key] .. ', ' .. alias) or alias; -- make comma-separated list of aliases
list[alias] = 'see ' .. key; -- make a back reference from this alias to the canonical parameter
end
end
end
end

for k, v in pairs (list) do -- loop through the list to make a simple unordered list
table.insert (out, table.concat ({'', k, ': ', v}));
end

table.sort (out); -- sort it
return table.concat (out, '\010'); -- concatenate with \n
-- return (mw.dumpObject (list))
end

--[[------------------------------------------------------------

experimental code that lists canonical parameter names. Perhaps basis for some sort of documentation?

returns a comma separated, alpha sorted, list of the canonical parameters. If given a template name, excludes
parameters listed in that template's exclusion_list[]{} table (if a table has been defined).

]]

local function canonical_param_lister (frame)
local template = frame.args[1];
if == template then
template = nil;
end

if template then
template = mw.text.trim (template:lower());
end

local alias_src = cfg.aliases; -- get master list of aliases
local id_src = cfg.id_handlers; -- get master list of identifiers

local list = {}; -- interim list of aliases
local out = {}; -- final output (for now an unordered list)

for _, aliases in pairs (alias_src) do -- loop through the master list of aliases
local name;
if 'table' == type (aliases) then -- table only when there are aliases
name = aliases[1]; -- first member of an aliases table is declared canonical
else
name = aliases; -- for those parameters that do not have any aliases, the parameter is declared canonical
end

if not template then -- no template name, add this parameter
table.insert (list, name);
elseif not exclusion_lists[template] then -- template name but no exclusion list
table.insert (list, name);
elseif not exclusion_lists[template][name] then -- template name and exclusion list but name not in list
table.insert (list, name);
end
end

for k, ids in pairs (id_src) do -- spin through the list of identifiers
local name = id_src[k].parameters[1]; -- get the first (left-most) parameter name
local access = id_src[k].custom_access; -- get the access-icon parameter if it exists for this identifier
if not template then -- no template name
table.insert (list, name); -- add this parameter
if access then
table.insert (list, access); -- add this access-icon parameter
end
elseif not exclusion_lists[template] then -- template name but no exclusion list
table.insert (list, name);
if access then
table.insert (list, access);
end
elseif not exclusion_lists[template][name] then -- template name and exclusion list but name not in list
table.insert (list, name);
if access then
table.insert (list, access);
end
end
end

for _, param in ipairs (list) do -- loop through the list to make a simple unordered list
table.insert (out, table.concat ({'', param}));
end

local function comp( a, b ) -- used in following table.sort()
return a:lower() ------------------------------------------

returns first (canonical) name when metaparameter is assigned a table of names
returns name when metaparameter is assigned a single name
returns empty string when metaparameter name not found in alias_src{}, id_src{}, or id_src[meta].custom_access

metaparameter is the key in Module:Citation/CS1 aliases{} table or id_handlers{} table. Because access-icon
don't have keys, per se, we create pseudo keys by appending 'access' to the identifier :
the for |doi-access= is, for the purposes of this function, DOIaccess, etc

Some lists of aliases might be better served when a particular alias is identified as the canonical alias for a
particular use case. If, for example, Perodical lists:
'journal', 'magazine', 'newspaper', 'periodical', 'website', 'work'
that order works fine for documentation but doesn't work so well for , ,
or . So, for using this function to document the returned value should be the
parameter best suited for that template so we can specify magazine in the override (frame.args[2])

While for this function, it would be just as simple to not use the function, this mechanism is implemented here
to match similar functionality in alias_names_get() (there are slight differences)
must exist in the alias list
does not apply to the access icon parameters (ignored - these have no aliases)

(and which would be best for ? |newspaper= or |work=? can't solve all of the worlds problems at once).

output format is controlled by |format=
plain - renders in plain text in a tag; may have id attribute
para - renders as it would in

]]

local function canonical_name_get (frame)
local alias_src = cfg.aliases; -- get master list of aliases
local id_src = cfg.id_handlers; -- get master list of identifiers
local args = getArgs (frame);

local name;
local meta = args[1]
local override = args[2];

local access; -- for id-access parameters
if meta:match ('^(%u+)access') then -- the metaparameter (which is not used in ~/Configuration) is id_handlers key concatenated with access: BIBCODEaccess
meta, access = meta:gsub ('^(%u+)access', '%1'); -- strip 'access' text from meta and use returned count value as a flag
end

if alias_src[meta] then
name = alias_src[meta]; -- name is a string or a table
if 'table' == type (name) then -- table only when there are aliases
if not override then
name = name[1]; -- first member of an aliases table is declared canonical
else
for _, v in ipairs (name) do -- here when override is set; spin throu the aliases to make sure override matches alias in table
if v == override then
name = v; -- declare override to be the canonical param for this use case
break;
end
end
end
end

elseif id_src[meta]then -- if there is an id handler
if access then -- and if this is a request for the handler's custom access parameter
if id_src[meta].custom_access then -- if there is a custom access parameter
name = id_src[meta].custom_access; -- use it
else
return ; -- nope, return empty string
end
else
if not override then
name = id_src[meta].parameters[1]; -- get canonical id handler parameter
else
for _, v in ipairs (id_src[meta].parameters) do -- here when override is set; spin throu the aliases to make sure override matches alias in table
if v == override then
name = v; -- declare override to be the canonical param for this use case
break;
end
end
end
end
else
return ; -- metaparameter not specified, or no such metaparameter
end

if 'plain' args.format then -- format and return the output
if args.id then
return string.format ('%s', args.id, name); -- plain text with id attribute
else
return name; -- plain text
end
elseif 'para' args.format then
return string.format ('|%s=', name); -- same as
end

return string.format ('%s', args.id or , name); -- because bolds param names
end

--[[--------------------------------------------------------------------------

returns list of aliases for metaparameter
returns empty string when there are no aliases
returns empty string when name not found in alias_src{} or id_src{}; access icon parameters have no aliases so ignored

metaparameter is the key in Module:Citation/CS1 aliases{} table or id_handlers{} table.

Some lists of aliases might be better served when a particular alias is identified as the canonical alias for a
particular use case. If, for example, Perodical lists:
'journal', 'magazine', 'newspaper', 'periodical', 'website', 'work'
that order works fine for documentation but doesn't work so well for , ,
or . So, for using this function to document the returned value should be the
aliases that are not best suited for that template so we can specify magazine in the override (frame.args[2])
to be the canonical parameter so it won't be listed with the rest of the aliases (normal canonical journal will be)

must exist in the alias list except:
when value is 'all', returns the canonical parameter plus all of the aliases

output format is controlled by |format=
plain - renders in plain text in a tag; may have id attribute
para - renders as it would in
when not specified, refurns the default bold format used for

]]

local function alias_names_get (frame)
local alias_src = cfg.aliases; -- get master list of aliases
local id_src = cfg.id_handlers; -- get master list of identifiers
local args = getArgs (frame);

local meta = args[1];
local override = args[2];

local out = {};
local source; -- selected parameter or id aliases list
local aliases;

source = alias_src[meta] or (id_src[meta] and id_src[meta].parameters);
if not source then
if meta:match ('%u+access') then
return 'no' args.none and or 'none'; -- custom access parameters don't have aliases
else
return ; -- no such meta
end
elseif not source[2] then -- id_source[meta] is always a table; if no second member, no aliases
return 'no' args.none and or 'none';
end

if not override then
aliases = source; -- normal skip-canonical param case
else
local flag = 'all' override and true or nil; -- so that we know that parameter is a valid alias; spoof when override 'all'
aliases = {[1] = }; -- spoof to push alias_src[meta][1] and id_src[meta][1] into aliases[2]
for _, v in ipairs (source) do -- here when override is set; spin through the aliases to make sure override matches alias in table
if v ~= override then
table.insert (aliases, v); -- add all but overridden param to the the aliases list for this use case
else
flag = true; -- set the flag so we know that is a valid alias
end
end
if not flag then
aliases = {} -- unset the table as error indicator
end
end

if 'table' == type (aliases) then -- table only when there are aliases
for i, alias in ipairs (aliases) do
if 1 ~= i then -- aliases[1] is the canonical name; don't include it
if 'plain' args.format then -- format and return the output
table.insert (out, alias); -- plain text
elseif 'para' args.format then
table.insert (out, string.format ('|%s=', alias)); -- same as
else
table.insert (out, string.format ("%s", alias)); -- because csdoc bolds param names
end
end
end

return table.concat (out, ', '); -- make pretty list and quit
end

return 'no' == args.none and or 'none'; -- no metaparameter with that name or no aliases
end

----------------------------------------------------------------

fetch the title of the current page; if it is a preprint template, return true; empty string else

local book_cite_templates = {
['citation'] = true,
['cite book'] = true,
}

local function is_book_cite_template ()
local title = mw.title.getCurrentTitle().rootText; -- get title of current page without namespace and without sub-pages; from Template:Cite book/new -> Cite book

title = title and title:lower() or ;
return book_cite_templates[title] or ;
end

--------------------------------------------------------

fetch the title of the current page; if it is a preprint template, return true; empty string else

local limited_param_templates = { -- if ever there is a need to fetch info from ~/Whitelist then
['cite arxiv'] = true, -- this list could also be fetched from there
['cite biorxiv'] = true,
['citeseerx'] = true,
['ssrn'] = true,
}

local function is_limited_param_template ()
local title = mw.title.getCurrentTitle().rootText; -- get title of current page without namespace and without sub-pages; from Template:Cite book/new -> Cite book

title = title and title:lower() or ;
return limited_param_templates[title] or ;
end

------------------------------------------------------------------------------------

makes a section header from and ; defaults to 2; cannot be less than 2

local function _header_make (args)
if not args[1] then
return ; -- no header text
end

local level = args[2] and tonumber (args[2]) or 2;

level = string.rep ('=', level);
return level .. args[1] .. level;
end

------------------------------------------------------------------------------------

Entry from an
makes a section header from and ; defaults to 2; cannot be less than 2

local function header_make (frame)
local args = getArgs (frame);
return _header_make (args);
end

--------------------------------------------------------------------------------

return the limit values for named identifier parameters that have limits (pmc, pmid, ssrn, s2cid, oclc, osti, rfc); the return
value used in template documentation and error message help-text

local function id_limits_get (frame)
local args = getArgs (frame);
local handlers = cfg.id_handlers; -- get id_handlers {} table from ~/Configuration

return args[1] and handlers[args[1]:upper()].id_limit or ('No limit defined for identifier: ' .. (args[1] or ) .. );
end

--------------------------------------------------------------------------------

local function cat_link_make (cat)
return table.concat ({':Category:', cat, });
end

------------------------------------------------------------------------

utility function to get script-language categories

local lang_list_t = mw.language.fetchLanguageNames ('en', 'all');

local function script_cat_lister (script_lang_codes_t, lang_tag_remap_t, cats_list_t)
for _, lang_code in ipairs (script_lang_codes_t) do
local lang_name = lang_tag_remap_t[lang_code] or lang_list_t[lang_code]; -- use remap table to get Bengali instead of Bangla and the like; else use standard MediaWiki names
local cat = 'CS1 uses ' .. lang_name .. '-language script (' .. lang_code .. ')'; -- build a category name
cats_list_t[cat] = 1; -- and save it
end
end

--select= -- (required) takes one of three values: error, maint, prop
|sandbox= -- takes one value: no
|hdr-lvl= -- base header level (number of == that make a header); default:2 min:2

This tool will automatically attempt to load a sandbox version of ~/Configuration if one exists.
Setting |sandbox=no will defeat this.

local function cat_lister (frame)
local args = getArgs (frame);

local list_live_cats = {}; -- list of live categories
local list_sbox_cats = {}; -- list of sandbox categories

local live_sbox_out = {} -- list of categories that are common to live and sandbox modules
local live_not_in_sbox_out = {} -- list of categories in live but not sandbox
local sbox_not_in_live_out = {} -- list of categories in sandbox but not live

local out = {}; -- final output assembled here

local sandbox; -- boolean; true: evaluate the sandbox module
local hdr_lvl; --

local sb_cfg;
local sandbox, sb_cfg = pcall (mw.loadData, 'Module:Citation/CS1/Configuration/sandbox'); -- get sandbox configuration

local cat;

local select = args.select;
if 'no' == args.sandbox then -- list sandbox?
sandbox = false; -- no, live only
end
if hdr_lvl then -- if set and
if tonumber (hdr_lvl) then -- can be converted to number
if 2 > tonumber (hdr_lvl) then -- min is 2
hdr_lvl = 2; -- so set to min
end
else -- can't be converted
hdr_lvl = 2; -- so default to min
end
else
hdr_lvl = 2; -- not set so default to min
end

if 'error' select or 'maint' select then -- error and main categorys handling different from poperties cats
for _, t in pairs (cfg.error_conditions) do -- get the live module's categories
if ('error' select and t.message) or ('maint' select and not t.message) then
cat = t.category:gsub ('|(.)$', ); -- strip sort key if any
list_live_cats[cat] = 1; -- add to the list
end
end

if sandbox then -- if ~/sandbox module exists and |sandbox= not set to 'no'
for _, t in pairs (sb_cfg.error_conditions) do -- get the sandbox module's categories
if ('error' select and t.message) or ('maint' select and not t.message) then
cat = t.category:gsub ('|(.)$', ); -- strip sort key if any
list_sbox_cats[cat] = 1; -- add to the list
end
end
end

elseif 'prop' == select then -- prop cats
for _, cat in pairs (cfg.prop_cats) do -- get the live module's categories
cat = cat:gsub ('|(.)$', ); -- strip sort key if any
list_live_cats[cat] = 1; -- add to the list
end

script_cat_lister (cfg.script_lang_codes, cfg.lang_tag_remap, list_live_cats); -- get live module's foriegn language script cats

if sandbox then -- if ~/sandbox module exists and |sandbox= not set to 'no'
for _, cat in pairs (sb_cfg.prop_cats) do -- get the sandbox module's categories
cat = cat:gsub ('|(.)$', ); -- strip sort key if any
list_sbox_cats[cat] = 1; -- add to the list
end

script_cat_lister (sb_cfg.script_lang_codes, sb_cfg.lang_tag_remap, list_sbox_cats); -- get sandbox module's foriegn language script cats
end
else
return 'error: unknown selector: ' .. select ..
end

for k, _ in pairs (list_live_cats) do -- separate live/sbox common cats from cats not in sbox
if not list_sbox_cats[k] and sandbox then
table.insert (live_not_in_sbox_out, cat_link_make (k)); -- in live but not in sbox
else
table.insert (live_sbox_out, cat_link_make (k)); -- in both live and sbox
end
end

for k, _ in pairs (list_sbox_cats) do -- separate sbox/live common cats from cats not in live
if not list_live_cats[k] then
table.insert (sbox_not_in_live_out, cat_link_make (k)); -- in sbox but not in live
end
end

local function comp (a, b) -- local function for case-agnostic category name sorting
return a:lower() ' -- opening for columns
});

table.sort (live_sbox_out, comp); -- sort case agnostic acsending
table.insert (live_sbox_out, 1, header); -- insert the header at the top
table.insert (out, table.concat (live_sbox_out, '\n')); -- make a big string of unordered list markup
table.insert (out, '\n'); -- close the and add new line so the next header works

if 0 ~= live_not_in_sbox_out then -- when there is something in the table
header = table.concat ({ -- build header for subsection
'In live but not in sandbox (',
live_not_in_sbox_out,
')'
});

header = table.concat ({ -- make a useable header
_header_make ({header, hdr_lvl+1}),
'\n' .. templatestyles ..
});

table.sort (live_not_in_sbox_out, comp);
table.insert (live_not_in_sbox_out, 1, header);
table.insert (out, table.concat (live_not_in_sbox_out, '\n'));
table.insert (out, '\n');
end

if 0 ~= sbox_not_in_live_out then -- when there is something in the table
header = table.concat ({ -- build header for subsection
'In sandbox but not in live (',
sbox_not_in_live_out,
')'
});

header = table.concat ({ -- make a useable header
_header_make ({header, hdr_lvl+1}),
'\n' .. templatestyles ..
});

table.sort (sbox_not_in_live_out, comp);
table.insert (sbox_not_in_live_out, 1, header);
table.insert (out, table.concat (sbox_not_in_live_out, '\n'));
table.insert (out, '\n');
end

return table.concat (out); -- concat into a huge string and done
end

--[=[----------------------------------------------------------------------------

To create category links at the bottom of each error help text section and on the individual error category pages;
fetches category names from ~/Configuration; replaces this:
|Category:CS1 errors: bioRxiv|Category:CS1 errors: bioRxiv|:Category:CS1 errors: bioRxiv}}
with this:

where } is the error_conditions key from Module:Citation/CS1/Configuration

add |pages=yes to append the number of pages in the category

]=]

local function help_text_cats (frame)
local args_t = getArgs (frame);
local error_conditions_t = cfg.error_conditions; -- get the table of error conditions
local replacements_t = {}; -- table to hold replacement parameters for $1 etc placeholders in category names
for k, v in pairs (args_t) do -- look for |$1= parameters
if 'string' == type (k) and k:match ('^$%d+$') then -- if found
replacements_t[k] = v; -- save key and value
end
end

if args_t[1] and error_conditions_t[args_t[1]] then -- must have error_condition key and it must exist
local error_cat = error_conditions_t[args_t[1]].category; -- get error category from cs1|2 configuration
if error_cat:match ('$%d') then -- look for placeholders in
error_cat = error_cat:gsub ('$%d', replacements_t) -- replace place holders with matching value from replacements_t
end

local title_obj = mw.title.getCurrentTitle(); -- get a title object for the currently displayed page
local name_space = title_obj.nsText;
if ('Category' name_space) and (error_cat title_obj.text) then -- if this is the category page for the error message
return table.concat ({'Category:', error_cat}); -- no link; just category name
else -- here when currently displayed page is other than the error message category
local pages = ; -- default empty strin for concatenation
if 'yes' == args_t.pages then -- if we should display category page count: TODO: do we need to keep this?
pages = mw.site.stats.pagesInCategory (error_cat, 'all'); -- get category page count
pages = table.concat ({' (', mw.language.getContentLanguage():formatNum (pages), ' page', (1 == pages) and ')' or 's)'}); -- make renderable text
end
return table.concat ({':Category:', error_cat, , pages}); -- link to category with or without page count
end
else
return 'unknown error_conditions key: ' .. (args_t[1] or 'key missing') .. ;
end
end

--$n= parameters to insert an empty string in the error message:
-> Check |issn= value
-> Check |eissn= value

error message is rendered at 120% font size; to specify another font size use |size=; must include unit specifier (%, em, etc)

local function help_text_error_messages (frame)
local args_t = getArgs (frame);
local error_conditions = mw.loadData ('Module:Citation/CS1/Configuration').error_conditions;
-- local span_o = ;
local span_o = ;
local span_c = ;

local message;
local out = {}; -- output goes here

if args_t[1] and error_conditions[args_t[1]] then -- must have error_condition key and it must exist
message = error_conditions[args_t[1]].message;
local i=1;
local count;
local rep;
repeat
rep = '$'..i
args_t[rep] = args_t[rep] and args_t[rep]:gsub ('^%s_%s$', ) or nil; -- replace empty string marker with actual empty string
message, count = message:gsub (rep, args_t[rep] or rep)
i = i + 1;
until (0 == count);

table.insert (out, span_o);
table.insert (out, message);
table.insert (out, span_c);
else
return 'unknown error_conditions key: ' .. (args_t[1] or 'key missing') .. ;
end

local out_str = table.concat (out);
return table.concat ({frame:extensionTag ('templatestyles', , {src='Module:Citation/CS1/styles.css'}), out_str});
end

--[[----------------------------------------------------------------------------------

This table is a k/v table of sequence tables. The keys in this table are collapsed lowercase form of the cs1|2
template names ():
Template:Cite AV media -> citeavmedia

Each subsequence table holds:
[1] documentation page where the TemplateData json is stored ( is the oddball)
[2] key to 'preprint_arguments_t' and unique_arguments_t' tables in Module:Citation/CS1/Whitelist; these keys
dictate which of the basic or limited arguments and numbered arguments tables will be used to validate
the content of the TemplateData

]]

local templates_t = {
citearxiv = {'Bản mẫu:Chú thích arXiv/doc', 'arxiv'}, -- preprint arguments
citeavmedia = {'Bản mẫu:Chú thích video/doc', 'audio-visual'}, -- unique arguments
citeavmedianotes = {'Bản mẫu:Chú thích ghi chú album/doc'}, -- no template data
citebiorxiv = {'Bản mẫu:Chú thích bioRxiv/doc', 'biorxiv'}, -- preprint arguments
citebook = {'Bản mẫu:Chú thích sách/TemplateData'},
citeciteseerx = {'Bản mẫu:Chú thích CiteSeerX/doc', 'citeseerx'}, -- no template data; preprint uses limited arguments
citeconference = {'Bản mẫu:Chú thích hội thảo/doc', 'conference'}, -- unique arguments
citedocument = {'Bản mẫu:Chú thích tài liệu/doc', 'document'}, -- special case; uses whitelist.document_parameters_t
citeencyclopedia = {'Bản mẫu:Chú thích bách khoa toàn thư/doc'},
citeepisode = {'Bản mẫu:Chú thích phần chương trình/doc', 'episode'}, -- unique arguments
citeinterview = {'Bản mẫu:Chú thích phỏng vấn/doc'},
citejournal = {'Bản mẫu:Chú thích tập san học thuật/doc'},
citemagazine = {'Bản mẫu:Chú thích tạp chí/doc'},
citemailinglist = {'Bản mẫu:Chú thích danh sách thư/doc', 'mailinglist'}, -- unique arguments -- no template data
citemap = {'Bản mẫu:Chú thích bản đồ/TemplateData', 'map'}, -- unique arguments
citemedrxiv = {'Bản mẫu:Chú thích medRxiv/doc', 'medrxiv'}, -- preprint arguments
citenews = {'Bản mẫu:Chú thích báo/doc'},
citenewsgroup = {'Bản mẫu:Chú thích nhóm tin/doc', 'newsgroup'}, -- unique arguments
citepodcast = {'Bản mẫu:Chú thích podcast/doc'},
citepressrelease = {'Bản mẫu:Chú thích thông cáo báo chí/doc'},
citereport = {'Bản mẫu:Chú thích báo cáo/doc', 'report'}, -- unique arguments
citeserial = {'Bản mẫu:Chú thích loạt chương trình/doc', 'serial'}, -- unique arguments -- no template data
citesign = {'Bản mẫu:Chú thích bảng thông tin/doc'},
citespeech = {'Bản mẫu:Chú thích diễn văn/doc', 'speech'}, -- unique arguments -- no template data
citessrn = {'Bản mẫu:Chú thích SSRN/doc', 'ssrn'}, -- preprint arguments -- no template data
citetechreport = {'Bản mẫu:Chú thích báo cáo kỹ thuật/doc'},
citethesis = {'Bản mẫu:Chú thích luận văn/doc', 'thesis'}, -- unique arguments
citeweb = {'Bản mẫu:Chú thích web/doc'},
citation = {'Bản mẫu:Chú thích/doc'},
}

--------------------------------------------------------------------

local no_page_templates_t = {};

------------------------------------------------------------------

a table of the identifier aliases

local identifier_aliases_t = {}
for identifier, handler in pairs (cfg.id_handlers) do -- for each identifier
local aliases_t = {}; -- create a table
for _, alias in ipairs (handler.parameters) do -- get the alaises
aliases_t[alias] = true; -- and add them to the table in a form that mimics the whitelist tables
end
identifier_aliases_t[identifier:lower()] = aliases_t; -- add new table to the identifier aliases table; use lowercase identifier base name for the key
end

--------------------------------------------------------------

get template doc page content and extract the content of the TemplateData tags (case insensitive)

is the canonical name of the template doc page (with namespace) that holds the template data; usually
Template:Cite xxx/doc (except Template:Cite book/TemplateData)

local function template_data_json_get (template)
local json = mw.title.new (template):getContent() or ; -- get the content of the article or ; new pages edited w/ve do not have 'content' until saved; ve does not preview; phab:T221625
json = json:match ('(.-)'); -- remove everything exept the content of the TemplatData tags
return json and mw.text.jsonDecode (json); -- decode the json string and return as a table; nil if not found
end

------------------------------------------------------------

looks for (can be the canonical parameter name or can be an alias) in whitelist.document_parameters_t.
When found, returns true; nil else

is the parameter's name as listed in the TemplateData

local function validate_document_param (param)
if true whitelist.document_parameters_t[param] then
return true;
end
end

----------------------------------------------------------------

looks for (can be the canonical parameter name or can be an alias) in whitelist.basic_arguments{} and if
necessary in whitelist.numbered_arguments{}. When found, returns true; nil else

is the parameter's name as listed in the TemplateData

local function validate_basic_param (param)
if true whitelist.common_parameters_t[param] then
return true;
end
end

------------------------------------------------------------

looks for (can be the canonical parameter name or can be an alias) in whitelist.preprint_arguments_t{} or
whitelist.limited_basic_arguments{} or whitelist.limited_numbered_arguments{}. When found, returns true; nil else

is the parameter's name as listed in the TemplateData
is key neccessary to look in the appropriate subtable of whitelist.preprint_arguments_t{}

local function validate_preprint_param (param, key)
if true whitelist.preprint_arguments_t[key][param] or
true whitelist.limited_parameters_t[param] then
-- true whitelist.limited_basic_arguments_t[param] or
-- true whitelist.limited_numbered_arguments_t[param] then
return true;
end
end

----------------------------------------------------------------

looks for (can be the canonical parameter name or can be an alias) in whitelist.unique_arguments_t{} or
whitelist.basic_arguments{} or whitelist.numbered_arguments{}. When found, returns true; nil else

is the parameter's name as listed in the TemplateData
is key neccessary to look in the appropriate subtable of whitelist.unique_arguments_t{}

local function validate_unique_param (param, key, cfg_aliases_t)
if true whitelist.unique_arguments_t[key][param] or true validate_basic_param (param) then
return true;
end
end

------------------------------------------------------------------------

looks for in identifier_aliases_t{}. When found, returns true; nil else

is the parameter's name as listed in the TemplateData
is the alias that we're looking for

local function validate_id_alias (param, alias)
return identifier_aliases_t[param] and identifier_aliases_t[param][alias];
end


-----------------------------------------------------------------------------

local function param_error_msg (param)
return '|' .. param .. '= is not a valid parameter';
end

---------------------------------------------------------------------

local function dup_alias_error_msg (param, alias)
return '|' .. param .. '= has duplicate aliases: |' .. alias .. '=';
end

------------------------------------------------------------------------

create an associative array of aliases. if already present in add an error message
to

local function dup_aliases_check (param, alias, aliases_t, out_t)
if not aliases_t[alias] then
aliases_t[alias] = true;
else
table.insert (out_t, dup_alias_error_msg (param, alias));
end
end

-----------------------------------------------------------------------------

local function alias_error_msg (param, alias)
return '|' .. alias .. '= is not a valid alias of: |' .. param .. '=';
end

--[[--------------------------------------------------------------------

convert this from cfg.aliases{}:
['AccessDate'] = {'access-date', 'accessdate'}

to this in out_t{}
['access-date'] = 'AccessDate',
['accessdate'] = 'AccessDate',

to test if |accessdate= is an aliases of |access-date=:
if out_t['access-date'] == out_t['accessdate']
]]

local function cfg_aliasts_t_make ()
local out_t = {};
for meta, params_t in pairs (cfg.aliases) do
if 'table' == type (params_t) then -- metaparameters that are assigned string values do not have aliases
for _, param in ipairs (params_t) do -- for each alias
param = param:gsub ('', ); -- get rid of enumerators
out_t[param] = meta; -- add it to the output table
end
end
end
--error (mw.dumpObject (out_t))
return out_t;
end

--2 template's TemplateData structure (everything between
and tag case insensitive). Returns error messages when errors found, empty string else.

}}

When called from a different page:

where the is the template's canonical name with or without namespace and or subpages

local function template_data_validate (frame)
local args_t = getArgs (frame);

if not args_t[1] then
return 'Error: cs1|2 template name required';
end

local template_idx = args_t[1]:lower():match ('cit[ae][^/]+'); -- args_t[1] has something
if not template_idx then -- but if not a cs1|2 template abandon with error message
return 'Error: cs1|2 template name required';
else
template_idx = template_idx:gsub (' ', ); -- is what appears to be a cs1|2 template so strip spaces
end

local cfg_aliases_t = cfg_aliasts_t_make ();

local template_t = templates_t[template_idx];
local out = {};

local template_doc = template_t[1];
local json_t = template_data_json_get (template_doc);

if not json_t then
table.insert (out, 'Error: can\'t find TemplateData');
else
for param, param_t in pairs (json_t['params']) do
local param_i; -- this will be the parameter name that gets validated
if param:find ('[Ss]2[Cc][Ii][Dd]') then -- |s2cid= parameters are not enumerated ...
param_i = param; -- ... so don't convert the '2' to ''
else
param_i = param:gsub ('%d+', ''); -- for enumerated parameters, convert the enumerator digits to a single '' character; all others unmolested
end

local param_is_valid; -- boolean true when param is valid; nil else
if template_t[2] then -- if template is a preprint or uses unique parameters of 'document' parameters
if 'document' == template_t[2] then -- if a template
param_is_valid = validate_document_param (param_i, template_t[2]);
if param_is_valid then
local aliases_t = {}; -- used by dup_aliases_check
if param_t['aliases'] then
for _, alias in ipairs (param_t['aliases']) do
dup_aliases_check (param, alias, aliases_t, out);
local alias_i = alias:gsub ('%d+', ''); -- in case an enumerated parameter, convert the enumerator digits to a single '' character
if not validate_document_param (alias_i, template_t[2]) then -- is 'alias' a known parameter?
table.insert (out, alias_error_msg (param, alias)); -- may be known but is not supported
elseif cfg_aliases_t[param_i:gsub ('', )] ~= cfg_aliases_t[alias_i:gsub ('', )] then -- is 'alias' known to be an alias of 'param'?
table.insert (out, alias_error_msg (param, alias));
end
end
end
else -- here when param not valid preprint param
table.insert (out, param_error_msg (param))
end
elseif whitelist.preprint_arguments_t[template_t[2]] then -- if a preprint template
param_is_valid = validate_preprint_param (param_i, template_t[2]);
if param_is_valid then
local aliases_t = {}; -- used by dup_aliases_check
if param_t['aliases'] then
for _, alias in ipairs (param_t['aliases']) do
dup_aliases_check (param, alias, aliases_t, out);
local alias_i = alias:gsub ('%d+', ''); -- in case an enumerated parameter, convert the enumerator digits to a single '' character
if not validate_preprint_param (alias_i, template_t[2]) then -- is 'alias' a known parameter?
table.insert (out, alias_error_msg (param, alias)); -- may be known but is not supported
elseif cfg_aliases_t[param_i:gsub ('', )] ~= cfg_aliases_t[alias_i:gsub ('', )] then -- is 'alias' known to be an alias of 'param'?
table.insert (out, alias_error_msg (param, alias));
end
end
end
else -- here when param not valid preprint param
table.insert (out, param_error_msg (param))
end
elseif whitelist.unique_arguments_t[template_t[2]] then -- if a unique parameters template
param_is_valid = validate_unique_param (param_i, template_t[2]);
if param_is_valid then
local aliases_t = {}; -- used by dup_aliases_check
if param_t['aliases'] then
for _, alias in ipairs (param_t['aliases']) do
dup_aliases_check (param, alias, aliases_t, out);
local alias_i = alias:gsub ('%d+', ''); -- in case an enumerated parameter, convert the enumerate digits to a single '' character
if not validate_unique_param (alias_i, template_t[2]) then -- is 'alias' a known parameter?
table.insert (out, alias_error_msg (param, alias));
elseif cfg_aliases_t[param_i:gsub ('', )] ~= cfg_aliases_t[alias_i:gsub ('', )] then -- is 'alias' known to be an alias of 'param'?
table.insert (out, alias_error_msg (param, alias));
end
end
end
else -- here when param not valid unique parameter
table.insert (out, param_error_msg (param))
end
else -- should never be here if coder is doing the right thing ...
table.insert (out, 'internal error: unexpected keyword in templates_t: ' .. template_t[2]);
break;
end
else -- here when not unique or preprint
param_is_valid = validate_basic_param (param_i);
if param_is_valid then
local aliases_t = {}; -- used by dup_aliases_check
if param_t['aliases'] then
for _, alias in ipairs (param_t['aliases']) do
dup_aliases_check (param, alias, aliases_t, out)
local alias_i = alias:gsub ('%d+', ''); -- in case an enumerated parameter, convert the enumerate digits to a single '' character
if not validate_basic_param (alias_i) and not validate_id_alias (param, alias) then -- for isbn13 (while still supported) must not mask the digits
table.insert (out, alias_error_msg (param, alias));
elseif cfg_aliases_t[param_i:gsub ('', )] ~= cfg_aliases_t[alias_i:gsub ('', )] then -- is 'alias' known to be an alias of 'param'?
table.insert (out, alias_error_msg (param, alias));
end
end
end
else -- here when param not valid
table.insert (out, param_error_msg (param))
end
end
end
end

---------- this emits errors when page/pages/at listed in templatedata of templates that don't support those parameters ----------
-- if json_t then
-- if ({['citeavmedia']=true, ['citeepisode']=true, ['citemailinglist']=true, ['citenewsgroup']=true, ['citepodcast']=true, ['citeserial']=true, ['citesign']=true, ['citespeech']=true})[template_idx] then
-- local insource_params_t = {}; -- build sequence of pagination params not supported by these templates
-- for _, meta_param in ipairs ({'At', 'Page', 'Pages', 'QuotePage', 'QuotePages'}) do
-- if 'table' == type (cfg.aliases[meta_param]) then
-- for _, alias in ipairs (cfg.aliases[meta_param]) do -- metaparameter is a sequence
-- table.insert (insource_params_t, alias); -- add the aliases from the metaparameter sequence to the table
-- end
-- else -- metaparameter is plain text
-- table.insert (insource_params_t, cfg.aliases[meta_param]); -- add the alias to the table
-- end
-- end
--
-- for _, param in ipairs (insource_params_t) do
-- if json_t.params[param] then
-- table.insert (out, param_error_msg (param)); -- error; this parameter not supported by this template
-- end
-- end
-- end
-- end
---------- end page/pages/at error detection ----------

if 0 ~= out then
table.sort (out);
out[1] = '' .. out[1]; -- add a splat to the first error message

-- return table.concat ({ .. template_doc .. ' TemplateData has errors:\n', table.concat (out, '\n'), });
return table.concat ({
'[[Template:' .. args_t[1] .. ']] uses ',
whitelist.preprint_arguments_t[template_t[2]] and 'preprint and limited parameter sets' or (whitelist.unique_arguments_t[template_t[2]] and 'unique and standard parameter sets' or 'standard parameter set'),
'; TemplateData has errors:\n',
'\n', table.concat (out, '\n'),
});
else
return; -- no errors detected; return nothing
end
end

------------------------------------------------------------------

loop through Module:Citation/CS1/Configuration error_conditions {} fetching error category names. For each error
category add the number of pages in the category to the tally. Render the number when done.

local function error_cat_page_tally ()
local error_conditions_t = cfg.error_conditions; -- get the table of error conditions
local tally = 0;
local cat_t = {}; -- some error message share a category; save tallied cats here so we don't recount the already counted
local i = 0; -- number of categories

for k, v_t in pairs (error_conditions_t) do
if k:match ('^err') then
if not cat_t[v_t.category] then
cat_t[v_t.category] = true;
tally = tally + mw.site.stats.pagesInCategory (v_t.category, 'pages'); -- get category page count; ignore subcats and files
i = i + 1;
end
end
end

return mw.language.getContentLanguage():formatNum (tally)
end

------------------------------------------------------------------

loop through Module:Citation/CS1/Configuration error_conditions {} fetching error category names. For each error
category add the number of pages in the category to the tally. Render the number when done.

Dynamic subcats of CS1 maint: DOI inactive not counted because these names come and go as time goes by.

local function maint_cat_page_tally ()
local error_conditions_t = cfg.error_conditions; -- get the table of error conditions
local tally = 0;
local cat_t = {}; -- some error message share a category; save tallied cats here so we don't recount the already counted
local i = 0; -- number of categories

for k, v_t in pairs (error_conditions_t) do
if not k:match ('^err') then -- if not an error key its a maint key
if not cat_t[v_t.category] then
cat_t[v_t.category] = true;
if 'maint_mult_names' k or 'maint_numeric_names' k then
local special_case_translation_t = cfg.special_case_translation;
for _, name in ipairs ({'AuthorList', 'ContributorList', 'EditorList', 'InterviewerList', 'TranslatorList'}) do
local cat_name = v_t.category:gsub ('$1', special_case_translation_t[name]); -- replace $1 with translated list name
tally = tally + mw.site.stats.pagesInCategory (cat_name, 'pages'); -- get category page count; ignore subcats and files
i = i + 1;
end
else
tally = tally + mw.site.stats.pagesInCategory (v_t.category, 'pages'); -- get category page count; ignore subcats and files
i = i + 1;
end
end
end
end

return mw.language.getContentLanguage():formatNum (tally)
end

--2 does not categorize.

For convenience,

returns a list of all namespace names and identifiers used on the current wiki. Any namespace with an
identifier less than 1, currently Mainspace (0), Special (-1), and Media (-2), is excluded from the list.

local function uncategorized_namespace_lister (frame)
local list_t = {};
local function compare (a, b) -- local function to sort namespaces numerically by the identifiers
local a_num = tonumber (a:match ('%d+')); -- get identifiers and convert to numbers
local b_num = tonumber (b:match ('%d+'));
return a_num : all namespace names and identifiers; ignore identifiers less than 1
table.insert (list_t, table.concat ({'', mw.site.namespaces[i].name, ' (', i, ')'})) -- add name and identifier as an unordered list item
end
end

table.sort (list_t, compare); -- ascending numerical sort by identifier

if not frame.args.all then -- when |all= not set, format list of uncategorized namespaces and identifiers
list_t[list_t] = 'and ' .. list_t[list_t]; -- add 'and ' to the last name/identifier pair
return table.concat (list_t, ', '); -- make a big string and done
else -- make list of all namespaces and identifiers
return table.concat (list_t, '\n'); -- make a big string and done
end
end

-----------------------------------------

for Help:CS1_errorsbad_url, list the supported top level domains that support single-letter 2nd level names



local function single_ltr_2nd_lvl_domain_lister ()
local out_t = {}; -- output goes here
for _, tld in ipairs (cfg.single_letter_2nd_lvl_domains_t) do -- fetch each tld
table.insert (out_t, '.' .. tld); -- prefix with a dot and save in out_t{}
end
return table.concat (out_t, ', '); -- make a big string and done
end

--[[------------------------------------------------------------------

Returns language code if pair exists, nil if either code doesn't exist or the name doesn't match.
Intended for use by Template:CS1 language sources/core

args[1] is language code
args[2] is language name

]]

local function code_name_pair_exists(frame)
local wiki_language = mw.getContentLanguage():getCode()
local source_list = mw.language.fetchLanguageNames(wiki_language, 'all');
local code_list = {};
local name_list = {};
local override = cfg.lang_tag_remap;

for code, name in pairs(source_list) do
add_to_list(code_list, name_list, override, code, name);
end

local args = getArgs(frame);
local language_code = args[1]
local language_name = args[2]
-- Check if the language code exists and the corresponding name matches
if code_list[language_code] == language_name then
-- Both code and name are a valid pair
return language_code
else
-- Either code doesn't exist or the name doesn't match
return nil
end
end

----------------------------------------------------------------------

return {
alias_lister = alias_lister,
alias_names_get = alias_names_get,
canonical_param_lister = canonical_param_lister,
canonical_name_get = canonical_name_get,
cat_lister = cat_lister,
code_name_pair_exists = code_name_pair_exists,
error_cat_page_tally = error_cat_page_tally,
header_make = header_make,
help_text_cats = help_text_cats,
help_text_error_messages = help_text_error_messages,
id_limits_get = id_limits_get,
is_book_cite_template = is_book_cite_template,
is_limited_param_template = is_limited_param_template,
lang_lister = lang_lister,
maint_cat_page_tally = maint_cat_page_tally,
script_lang_lister = script_lang_lister,
single_ltr_2nd_lvl_domain_lister = single_ltr_2nd_lvl_domain_lister,
template_data_validate = template_data_validate,
uncategorized_namespace_lister = uncategorized_namespace_lister,
};