มอดูล:Cs1 documentation support

require('Module:No globals');
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_listcode then													-- look in the override table for this code
		code_listcode = override_listcode .. dagger;						-- use the name from the override table; mark with dagger
		name_listoverride_listcode = code .. dagger;
	else
		code_listcode = name;													-- use the MediaWiki name and code
		name_listname = 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
--language= parameter
	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_code_remap;
	local code_1_list={};
	local code_2_list={};
	local ietf_list={};
	local name_list={};
	
	if not ({'2char'=true, '3char'=true, 'ietf'=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);
		else																	-- ietf codes only partically supported by cs1|2 |language= parameter
			add_to_list (ietf_list, name_list, override, code, name);
		end
	end
	
	local result = {};
	local out = {};
	if '2char' == frame.args.list or 'all' == frame.args.list then
		list_format (result, code_1_list);
	elseif '3char' == frame.args.list then
		list_format (result, code_2_list);
	elseif 'ietf' == frame.args.list then
		list_format (result, ietf_list);
	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_code_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
					listkey = listkey and (listkey .. ', ' .. alias) or alias;	-- make comma-separated list of aliases
					listalias = '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.args1;
	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 = aliases1;													-- 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_liststemplate then								-- template name but no exclusion list
			table.insert (list, name);
		elseif not exclusion_liststemplatename 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_srck.parameters1;									-- get the first (left-most) parameter name
		local access = id_srck.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_liststemplate then								-- template name but no exclusion list
			table.insert (list, name);
			if access then
				table.insert (list, access);
			end
		elseif not exclusion_liststemplatename 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_srcmeta.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.args2)
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 = args1
	local override = args2;
	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_srcmeta then
		name = alias_srcmeta;													-- name is a string or a table
		if 'table' == type (name) then											-- table only when there are aliases
			if not override then
				name = name1;													-- 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_srcmetathen														-- if there is an id handler
		if access then															-- and if this is a request for the handler's custom access parameter
			if id_srcmeta.custom_access then									-- if there is a custom access parameter
				name = id_srcmeta.custom_access;								-- use it
			else
				return ;														-- nope, return empty string
			end
		else
			if not override then
				name = id_srcmeta.parameters1;								-- get canonical id handler parameter
			else
				for _, v in ipairs (id_srcmeta.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.args2)
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 = args1;
	local override = args2;
	local out = {};
	local source;																-- selected parameter or id aliases list
	local aliases;
	source = alias_srcmeta or (id_srcmeta and id_srcmeta.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 source2 then													-- id_sourcemeta 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_srcmeta1 and id_srcmeta1 into aliases2
		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														-- aliases1 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_templatestitle 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_templatestitle or ;
end
------------------------------------------------------------------------------------
makes a section header from  and ;  defaults to 2; cannot be less than 2
local function _header_make (args)
	if not args1 then
		return ;																-- no header text
	end
	
	local level = args2 and tonumber (args2) or 2;
	
	level = string.rep ('=', level);
	return level .. args1 .. 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 args1 and handlersargs1:upper().id_limit or ('No limit defined for identifier: ' .. (args1 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_code_remap_t, cats_list_t)
	for _, lang_code in ipairs (script_lang_codes_t) do
		local lang_name = lang_code_remap_tlang_code or lang_list_tlang_code;	-- use remap table to get Bengali instead of Bangla and the like; else use standard MediaWiki names
		local cat = 'CS1 ใช้อักษรภาษา' .. lang_name .. ' (' .. lang_code .. ')';	-- build a category name
		cats_list_tcat = 1;													-- and save it
	end
end
--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_catscat = 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_catscat = 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_catscat = 1;											-- add to the list
		end
		script_cat_lister (cfg.script_lang_codes, cfg.lang_code_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_catscat = 1;										-- add to the list
			end
			script_cat_lister (sb_cfg.script_lang_codes, sb_cfg.lang_code_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_catsk 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_catsk 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:
	
with this:
	err_bad_biorxiv
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
	if args_t1 and error_conditions_targs_t1 then							-- must have error_condition key and it must exist
		local error_cat = error_conditions_targs_t1.category;				-- get error category from cs1|2 configuration
		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: ' .. (args1 or 'key missing') .. ;
	end
end
------------------------------------------------------------
to render help text example error messages
	err_bad_biorxiv
assign a single underscore to any of the |$n= parameters to insert an empty string in the error message:
	$1=_ -> Check |issn= value
	$1=e -> 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_t1 and error_conditionsargs_t1 then								-- must have error_condition key and it must exist
		message = error_conditionsargs_t1.message;
		local i=1;
		local count;
		local rep;
		repeat
			rep = '$'..i
			args_trep = args_trep and args_trep:gsub ('^%s*_%s*$', ) or nil;	-- replace empty string marker with actual empty string
			message, count = message:gsub (rep, args_trep 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_t1 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' and unique_arguments' 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 = {'Template:Cite_arXiv/doc', 'arxiv'},							-- preprint arguments 
	citeavmedia = {'Template:Cite AV media/doc', 'audio-visual'},				-- unique arguments
	citeavmedianotes = {'Template:Cite AV media notes/doc'},					-- no template data
	citebiorxiv = {'Template:Cite bioRxiv/doc', 'biorxiv'},						-- preprint arguments
	citebook = {'Template:Cite book/TemplateData'},
	citeciteseerx = {'Template:Cite citeseerx/doc', 'citeseerx'},				-- no template data; preprint uses limited arguments
	citeconference = {'Template:Cite conference/doc', 'conference'},			-- unique arguments
	citeencyclopedia = {'Template:Cite encyclopedia/doc'},
	citeepisode = {'Template:Cite episode/doc', 'episode'},						-- unique arguments
	citeinterview = {'Template:Cite interview/doc'},
	citejournal = {'Template:Cite journal/doc'},
	citemagazine = {'Template:Cite magazine/doc'},
	citemailinglist = {'Template:Cite mailing list/doc', 'mailinglist'},		-- unique arguments			-- no template data
	citemap = {'Template:Cite map/doc', 'map'},									-- unique arguments			-- no template data
	citenews = {'Template:Cite news/doc'},
	citenewsgroup = {'Template:Cite newsgroup/doc', 'newsgroup'},				-- unique arguments
	citepodcast = {'Template:Cite podcast/doc'},
	citepressrelease = {'Template:Cite press release/doc'},
	citereport = {'Template:Cite report/doc', 'report'},						-- unique arguments
	citeserial = {'Template:Cite serial/doc', 'serial'},						-- unique arguments			-- no template data
	citesign = {'Template:Cite sign/doc'},
	citespeech = {'Template:Cite speech/doc', 'speech'},						-- unique arguments			-- no template data
	citessrn = {'Template:Cite ssrn/doc', 'ssrn'},								-- preprint arguments		-- no template data
	citetechreport = {'Template:Cite techreport/doc'},
	citethesis = {'Template:Cite thesis/doc', 'thesis'},						-- unique arguments
	citeweb = {'Template:Cite web/doc'},
	citation = {'Template:Citation/doc'},
	}
------------------------------------------------------------------
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_talias = true;												-- and add them to the table in a form that mimics the whitelist tables
	end
	identifier_aliases_tidentifier: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.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.basic_argumentsparam or true == whitelist.numbered_argumentsparam then
		return true;
	end
end
------------------------------------------------------------
looks for  (can be the canonical parameter name or can be an alias) in whitelist.preprint_arguments{} 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{}
local function validate_preprint_param (param, key)
	if true == whitelist.preprint_argumentskeyparam or 
		true == whitelist.limited_basic_argumentsparam or
		true == whitelist.limited_numbered_argumentsparam then
			return true;
	end
end
----------------------------------------------------------------
looks for  (can be the canonical parameter name or can be an alias) in whitelist.unique_arguments{} 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{}
local function validate_unique_param (param, key, cfg_aliases_t)
	if true == whitelist.unique_argumentskeyparam 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_tparam and identifier_aliases_tparamalias;
end
	
-----------------------------------------------------------------------------
local function param_error_msg (param)
	return '|' .. param .. '= is not a valid parameter';
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_tparam = 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_t1 then
		return 'Error: cs1|2 template name required';
	end
	local template_idx = args_t1:lower():match ('citae^/+');				-- args_t1 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_ttemplate_idx;
	local out = {};
	local template_doc = template_t1;
	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 = param:gsub ('%d+', '#');							-- in case an enumerated parameter, convert the enumerate digits to a single '#' character
			local param_is_valid;												-- boolean true when param is valid; nil else
			if template_t2 then												-- if template is a preprint or uses unique parameters
				if whitelist.preprint_argumentstemplate_t2 then				-- if a preprint template
					param_is_valid = validate_preprint_param (param_i, template_t2);
					if param_is_valid then
						if param_t'aliases' then
							for _, alias in ipairs (param_t'aliases') do
								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_t2) then	-- is 'alias' a known parameter?
	 								table.insert (out, alias_error_msg (param, alias));			-- may be known but is not supported
								elseif cfg_aliases_tparam_i:gsub ('#', ) ~= cfg_aliases_talias_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_argumentstemplate_t2 then			-- if a unique parameters template
					param_is_valid = validate_unique_param (param_i, template_t2);
					if param_is_valid then
						if param_t'aliases' then
							for _, alias in ipairs (param_t'aliases') do
								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_t2) then	-- is 'alias' a known parameter?
									table.insert (out, alias_error_msg (param, alias));
								elseif cfg_aliases_tparam_i:gsub ('#', ) ~= cfg_aliases_talias_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_t2);
					break;
				end
			else																-- here when not unique or preprint
				param_is_valid = validate_basic_param (param_i);
				if param_is_valid then
					if param_t'aliases' then
						for _, alias in ipairs (param_t'aliases') do
							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));
							end
						end
					end
				else															-- here when param not valid
					table.insert (out, param_error_msg (param))
				end
			end
		end
	end
	if 0 ~= #out then
		table.sort (out);
		out1 = '*' .. out1;													-- 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_t1 .. '  uses ',
			whitelist.preprint_argumentstemplate_t2 and 'preprint and limited parameter sets' or (whitelist.unique_argumentstemplate_t2 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_tv_t.category then
				cat_tv_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_tv_t.category then
				cat_tv_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_tname);	-- 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
----------------------------------------------------------------------
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,
	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,
	template_data_validate = template_data_validate,
	};