モジュール:Footnotes/anchor id list

require('strict');
local data = mw.loadData ('Module:Footnotes/anchor id list/data');
local whitelist = mw.loadData ('Module:Footnotes/whitelist');
local Lang_obj = mw.language.getContentLanguage();								-- used by template_list_add() to uppercase first letter of template name TODO: better way to do that?
local redirects_date = {
	'date' = true,
	'datetomos' = true,
	'formatdate' = true,
	'isotodmymdy' = true,
	'isotomos' = true,
	}
local redirects_patent = {														-- special case cs1-like templates because uses different parameters for name and date in anchor ID
	'Cite patent' = true,
	'Citeref patent' = true,
	'Ref patent' = true,
	}
local redirects_sfnref = {
	'sfnref' = true,
	'harvid' = true,
	}
local aliases_author = {														-- these use pseudo-patterns in the same way as cs1|2; '#' represents 1 or more enumerator digits
	'last#',
	'author#',
	'surname#',
	'author-last#',
	'author#-last',
	'subject#',
	'host#',
	}
local aliases_contributor = {
	'contributor#',
	'contributor-last#',
	'contributor#-last',
	'contributor-surname#',
	'contributor#-surname',
	}
local aliases_editor = {
	'editor#',
	'editor-last#',
	'editor#-last',
	'editor-surname#',
	'editor#-surname',
	}
local aliases_harvc_author = {
	'last#',
	'author#',
	}
local aliases_inventor = {														-- cite patent
	'inventor#',
	'inventor-last#',
	'inventor#-last',
	'inventor-surname#',
	'inventor#-surname',
	'invent#',
	'invent-#',
	}
local alias_patterns_date = {													-- normal lua patterns for most cs1|2-like templates
	'|%s*year%s*=%s*',
	'|%s*date%s*=%s*',
	'|%s*publication%-?date%s*=%s*',
	'|%s*air%-?date%s*=%s*',
	}
local alias_patterns_harvc_date = {												-- normal lua patterns for harvc template
	'|%s*anchor%-year%s*=%s*',
	'|%s*year%s*=%s*',
	}
local alias_patterns_patent_date = {											-- normal lua patterns for cite patent templates
	'|%s*issue%-date%s*=%s*',
	'|%s*gdate%s*=%s*',
	'|%s*publication%-date%s*=%s*',
	'|%s*pubdate%s*=%s*',
	}
local patterns_date = {															-- normal lua patterns
	'(%d%d%d%d–%d%d%d%d%l?)$',													-- YYYY–YYYY four-digit year range at end (Season YYYY–YYYY); with or without dab
	'^(%d%d%d%d–%d%d%l?)$',														-- YYYY–YY two-digit year range; with or without dab
	'^(c%. %d%d%d%d?%l?)$',														-- three- or four-digit circa year; with or without dab
	'(%d%d%d%d?%l?)$',															-- three- or four-digit year at end of date (dmy or mdy); with or without dab
	'^(%d%d%d%d?%l?)',															-- three- or four-digit year at beginning of date (ymd or YYYY); with or without dab
	'^(n%.d%.%l?)$',															-- 'no date' with dots; with or without dab
	'^(nd%l?)$',																-- 'no date' without dots; with or without dab
	}
local patterns_tags = {
	,
	,
	'.-',
	'.-',
	'.-',													-- deprecated alias of syntaxhighlight tag
	}
local template_skip = {															-- templates to be skipped for whatever reason; mostly because they resemble cs1-like templates
	'Citation-attribution' = true,
	}
local Article_content;
local anchor_id_list = {};														-- exported tables
local template_list = {};
local article_whitelist = {};
--------------------------------------------------------------------
get article content, remove certain html-like tags and their content so that this code doesn't include any citation
templates inside the tags as valid tagets; they are not.
local function article_content_get ()
	if not Article_content then
		Article_content = mw.title.getCurrentTitle():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
		for _, tag in ipairs (patterns_tags) do
			Article_content = Article_content:gsub (tag, );					-- remove certain html-like tags and their content
		end
	end
end
--------------------------------------------------------------------------------------
make an anchor id from the contents of  or .  this function assumes that  and 
are correctly formed.
local function sfnref_get (template)
	template = template:gsub (, '%1');							-- strip bounding template markup and trim
	local parts = mw.text.split (template, '%s*|%s*');							-- split at the pipe and remove extraneous space characters
	local anchor_id = {};
	if redirects_sfnrefparts1:lower() then
		anchor_id1 = 'CITEREF';
	else
		return nil;																-- not an sfnref or harvid template
	end
	
	local i = 2;																-- indexer into parts{} table
	local j = 2;																-- indexer into anchor_id{} table which already has 'CITEREF' at 1
	while partsi and 7 > j do													-- loop through what should be just positional parameters for names and year (2-6 four names and a date)
		if not partsi:find ('=') then											-- look for equal sign (named paraneter in a template that doesn't support named parameters)
			anchor_idj = partsi;											-- positional parameters are saved
			j = j+1;															-- bump the anchor_id{} indexer
		end
		i = i+ 1;																-- bump the parts{} indexer
	end
	return table.concat (anchor_id, );
end
------------------------------------------------------------------------------------------
extract year from one of |year=, |date=, |publicationdate=, or |publication-date in that order.  Does not error
check (that is left to the cs1|2 templates to do)
also gets date from |=...
local function date_get (template, aliases)
	local date;
	local rvalue;
	for _, pattern in ipairs (aliases) do										-- spin through the date alias patterns
		rvalue = tostring(template):match (pattern);							-- is this |= used (tostring() because something makes match() think template is a table)
		if rvalue then
			rvalue = tostring(template):match (pattern .. '(%b{})');			-- is rvalue a template?
			if rvalue then
				rvalue = rvalue:gsub (, '%1');					-- strip bounding template markup and trim
				local parts = mw.text.split (rvalue, '%s*|%s*');				-- split at the pipe and remove extraneous space characters
				if redirects_dateparts1:lower() then						-- if parts1 names  or redirect
					rvalue = parts2;											-- assume that date template is properly formed, first positional parameter is the date
				else
					return ;													-- |date= holds some other template than  or redirect
				end
			else
				rvalue = template:match (pattern .. '(}+)');
				if rvalue then													-- if rvalue is something
					rvalue = mw.text.trim (rvalue);								-- trim it
				end
				
				if not rvalue or  == rvalue then								-- if rvale was nothing or trimed to nothing
					rvalue = nil;												-- ensure that it is unset so we can try the next parameter in the list
				end
			end
			if rvalue then
				for _, pattern in ipairs (patterns_date) do						-- spin through the recognized date formats
					date = rvalue:match (pattern);								-- attempt to extract year portion according to the pattern
					if date then
						return date;											-- matched so return;
					end
				end
				break;															-- found a date but it was malformed so abandon
			end
		end
	end
	return ;																	-- no date param or date param doesn't hold a recognized date; empty string for concatenation
end

--vcontributors= parameter.
splits the v parameter value at the comma; correctly handles accept-as-witten markup when used to wrap a comma-
separated names (corporate)
local function vnames_get (params, vparam)
	local vnames = {};															-- first four author or editor names go here
	local split = {};															-- temp table to assist in decoding accept-as-witten-markup
	if paramsvparam then														-- test for |vauthors= or |veditor=
		split = mw.text.split (paramsvparam, '%s*,%s*');						-- this will separate portions of ((Black, Brown, White, an Co.))
	
		local i = 1;															-- an indexer
		
		while spliti do
			if spliti:match ('^%(%(.*^%)^%)$') then						-- first segment of comma-separated accept-as-witten; this segment has the opening doubled parens
				local name = spliti;
				i=i+1;															-- bump indexer to next segment
				while spliti do
					name = name .. ', ' .. spliti;							-- concatenate with previous segments
					if spliti:match ('^.*%)%)$') then							-- if this table member has the closing doubled parens
						break;													-- and done reassembling so
					end
					i=i+1;														-- bump indexer
				end
				table.insert (vnames, name);									-- and add accept-as-witten name to the vnames table
	
			else
				table.insert (vnames, spliti);								-- and add name to the vnames table
			end
		i=i+1;																	-- bump indexer
		if 5 == i then break; end												-- limit to four names
		end
		for i, vname in ipairs (vnames) do
			if not vname:match ('%(%(.-%)%)') then								-- without accept-this-value-as-written markup
				vnamesi = vname:gsub ('(.-)%s+%u+$', '%1');					-- extract and save surname(s)
			end
		end
		for i, vname in ipairs (vnames) do										-- repeat, this time for accept-this-value-as-written markup
			vnamesi = vname:gsub ('%(%((.-)%)%)', '%1');						-- remove markup if present and save the whole name
		end
	end
	return 0 ~= #vnames and table.concat (vnames) or nil						-- return a concatenation of the vnames; nil else
end
--2 template;  if there are no contributor names, try author names, then try editor names.
returns concatenated names in enumeration order when successful; nil else
empty name (nameholding parameter n is present without value) and missing name (nameholding parameter n is not
present) are included as empty string with all other names
local function names_get (params, aliases_list)
	local names = {};															-- first four author or editor names go here
	local enum_alias;															-- alias with '#' replaced with a digit
	for enum=1, 4 do															-- four names only
		for i, alias in ipairs (aliases_list) do
			if not namesenum then												-- hanven't found a previous alias with this enum? see if we can find this alias with this enum
				enum_alias = alias:gsub ('#', enum);							-- replace '#' to make 'lastn'
	
				if 1 == enum then												-- because |last= and last1= are exact aliases
					if paramsenum_alias then									-- test |last1= first
						namesenum = paramsenum_alias;						-- found so save the value assigned to |last1=
						break;													-- next enum
					else
						enum_alias = alias:gsub ('#', );						-- replace '#' to make 'last'
						if paramsenum_alias then
							namesenum = paramsenum_alias;					-- found so save the value assigned to |last=
							break;												-- next enum
						end
					end
				else															-- here for enum 2, 3, 4
					if paramsenum_alias then
						namesenum = paramsenum_alias;						-- found so save the value assigned to |lastn=
						break;													-- next enum
					end
				end
			end
		end
	end
	for enum=1, 4 do															-- spin through the names table and
		local name = namesenum;
		if not name then														-- when nameholding parameter n is not present (nil)
			name = ;															-- convert to empty string for concatenation
		end
		name = name:gsub('%(%((.-)%)%)', '%1');									-- remove accept-as-written markup if present
		namesenum = name;														-- save the modified name
	end
	local name_str = table.concat (names);										-- concatenate the names
	return  ~= name_str and name_str or nil;									-- return the concatenation if not empty string; nil else
end
--last2= empty -> CITEREFLast1Last3YYYY (the harv and sfn render: 'Last1, & Last3 YYYY' with CITEREFLast1Last3YYYY).
local function template_strip (template)
	template = template:gsub ('^$', , 1);					-- remove outer  (cs1|2 template delimiters)
	template = template:gsub ('%b{}', );										-- remove any templates from the cs1|2 template
	return template;
end
--------------------------------------------------------------
Returns a string where all of lua's magic characters have been escaped.  This is important because functions like
string.gsub() treat their pattern and replace strings as patterns, not literal strings.
local function escape_lua_magic_chars (argument)
	argument = argument:gsub("%%", "%%%%");										-- replace % with %%
	argument = argument:gsub("(%^%$%(%)%.%%%*%+%-%?)", "%%%1");				-- replace all other lua magic pattern characters
	return argument;
end
--=---------------------------------------------------------------------------
Wikilink markup does not belong in an anchor id and can / does confuse the code that parses apart citation and
harvc templates so here we remove any wiki markup:
	label -> label
	link -> link
	
**
local function wikilink_strip (template)
	for wikilink in template:gmatch ('%%b%') do								-- get a wikilink
		template = template:gsub ('%%b%', '__57r1P__', 1);					-- install a marker
		if wikilink:match ('%%(.-)%%') then
			wikilink = wikilink:match ('%%(.-)%%');						-- extract label from complex label wikilink
		else
			wikilink = wikilink:match ('%%(.-)%%');							-- extract link from simple link wikilinks
		end
		wikilink = escape_lua_magic_chars (wikilink);							-- in case there are lua magic characters in wikilink
		template = template:gsub ('__57r1P__', wikilink, 1);					-- replace the marker with the appropriate text
	end
	return template;
end
------------------------------------------------------------------------
return the citation or harvc template's name; convert to lower case and trim leading and trailing whitespace;
when the template is a sandbox the subpage portion of the template name is omitted from the returned template name
	... returns cite book
local function template_name_get (template)
	local template_name = template:match ('^+)');						-- get the value; whitespace is a 'value'
			if ref then															-- nil when |ref=|... or when |ref=}} (no spaces between assignment operator and pipe or closing brace)
				ref = mw.text.trim (ref);										-- something, could be just whitespace, so trim leading / trailing whitespace
				if  == ref then												-- trimming a string of whitespace makes an empty string
					ref = nil;													-- make empty ref same as missing ref
				end
			end
		end
	end
	template_params_get (template, params);										-- build a table of template parameters and their values
	if whitelist.wrapper_templatestemplate_name1 then						-- is this wrapper a simple-default wrapper?
		name_default = whitelist.wrapper_templatestemplate_name1;			-- get the default names
		date_default = whitelist.wrapper_templatestemplate_name2;			-- get the default date
	else
		vol = params'volume' or 'default';
		if not whitelist.wrapper_templatestemplate_namevol then				-- make sure this volume exists
			vol = 'default';													-- doesn't exist, use default volume
		end
		name_default = whitelist.wrapper_templatestemplate_namevol1;		-- get the default names
		date_default = whitelist.wrapper_templatestemplate_namevol2;		-- get the default date
	end
	if 'harv' == ref  or not ref then											-- |ref=harv specified or |ref= missing or empty
		anchor_id = names_get (params, aliases_contributor) or					-- get contributor, author, or editor names
			names_get (params, aliases_author) or
			vnames_get (params, 'vauthors') or									-- |vauthors=
			names_get (params, aliases_editor) or
			vnames_get (params, 'veditors') or									-- |veditors=
			name_default;														-- default names from whitelist
--			whitelist.wrapper_templatestemplate_name1;						-- default names from whitelist
		if  == date then														-- if date not provided in the template
			date = date_default;												-- use the default date from whitelist
		end
		if anchor_id then														-- if names were gotten
			anchor_id = 'CITEREF' .. anchor_id .. date;
		end
	elseif ref:match ('%b{}') then												-- ref holds a template
		anchor_id = sfnref_get (ref);											-- returns content of  or ; nil else
	elseif 'none' == ref then													-- |ref=none
		return nil;																-- anchor id expicitly suppressed
		
	else
		anchor_id = ref;														-- |ref= may match an anchor id override value in  template |ref= parameter
	end
	
	return anchor_id;															-- anchor_id text; nil else
end
--------------------------------------------------------------------
for cs1|2 template and cs1-like templates
inspect |ref= to decide what to do:
	|ref=										- empty or missing: get names and date from template parameters; all cs1|2 create CITEREF anchor IDs
	|ref=harv									- get names and date from template parameters
	|ref=year	- assemble an anchor id from  positional parameters
	|ref=year	- assemble an anchor id from  positional parameters
	|ref=none									- skip; do nothing because an anchor id intentionally suppressed; TODO: keep with a type code of '0'?
	|ref=									- save param value because may match an anchor id override value in  template |ref= parameter or  
id= parameter
local function anchor_id_make_cs12 (template)
	local ref;																	-- content of |ref=
	local template_name;														-- name of the template
	local anchor_id;															-- the assembled anchor id from this template
	local date;
	local params = {};															-- table of template parameters
	
	template_name = template_name_get (template);								-- get first char uppercase trimmed template name; ignore subpages ~/new, ~/sandbox
	if not template_name or template_skiptemplate_name then
		return nil;																-- could not extract template name from (possibly corrupted) template (extraneous opening { in the template will cause this)
	end
	if redirects_patenttemplate_name then
		date = date_get (template, alias_patterns_patent_date);					-- get date; done here because might be in  
	else
		date = date_get (template, alias_patterns_date);
	end
	
	ref = template:match ('|%s*ref%s*=%s*(%b{})');								-- first look for |ref= or |ref= because we will strip templates from the cs1|2 template
	if not ref then
		if template:match ('|%s*ref%s*=(}+)') then							-- |ref= not found; if there is a |ref= param with an assigned value
			ref = template:match ('|%s*ref%s*=(}+)');						-- get the value; whitespace is a 'value'
			if ref then															-- nil when |ref=|... or when |ref=}} (no spaces between assignment operator and pipe or closing brace)
				ref = mw.text.trim (ref);										-- something, could be just whitespace, so trim leading / trailing whitespace
				if  == ref then												-- trimming a string of whitespace makes an empty string
					ref = nil;													-- make empty ref same as missing ref
				end
			end
		end
	end
	template_params_get (template, params);										-- build a table of template parameters and their values
	if not ref then																-- |ref= not set, might be cite LSA which doesn't support |ref=
		if 'cite lsa' == template_name then
			return 'CITEREF' .. (params.last or ) .. (params.year or );		-- cite LSA always creates an anchor id using only |last= and |year= (no aliases)
		end
	end
	if 'harv' == ref  or not ref then											-- |ref=harv specified or |ref= missing or empty
		if redirects_patenttemplate_name then									-- if this is a cite patent template
			anchor_id = names_get (params, aliases_inventor);					-- inventor names only
		else																	-- cs1|2 template
			anchor_id = names_get (params, aliases_contributor) or				-- get contributor, author, or editor names
				names_get (params, aliases_author) or
				vnames_get (params, 'vauthors') or								-- |vauthors=
				names_get (params, aliases_editor) or
				vnames_get (params, 'veditors');								-- |veditors=
		end
		if anchor_id then														-- if names were gotten
			anchor_id = 'CITEREF' .. anchor_id .. date;
		end
	elseif ref:match ('%b{}') then												-- ref holds a template
		anchor_id = sfnref_get (ref);											-- returns content of  or ; nil else
	elseif 'none' == ref and not redirects_patenttemplate_name then			-- |ref=none; not supported by cite patent
		return nil;																-- anchor id expicitly suppressed
		
	else
		anchor_id = ref;														-- |ref= may match an anchor id override value in  template |ref= parameter
	end
	
	return anchor_id;															-- anchor_id text; nil else
end
------------------------------------------------------------------------------------------
adds an  to  table; for anchor IDs, the boolean  argument must be set true; no return value
local function list_add (item, list, encode)
	if item then																-- if there was an item
		if encode then															-- for anchor IDs ...
			item = mw.uri.anchorEncode (item);									-- encode to remove wikimarkup, convert spaces to underscores etc
		end
		
		if not listitem then													-- if not already saved
			listitem = 1;														-- save it 
		else																	-- here when this item already saved
			listitem = listitem + 1;										-- to indicate that there are multiple items
		end
	end
end
----------------------------------------------------------------
make anchor IDs from ; there may be more than one because  is not limited to the number of
anchors it may hold.
local function anchor_id_make_anchor (template, anchor_id_list)
	template = template:gsub ('^', ):gsub ('$', , 1);				-- remove outer  and template name
	template = wikilink_strip (template);										-- strip any wikilink markup (there shouldn't be any but just in case)
	
	local params = {};
	local anchor_id;
	
	for param in template:gmatch ('%b{}') do									-- loop through the template; remove and save templates (presumed to be sfnref or harvid)
		table.insert (params, param);											-- save it
		template = template:gsub ('%b{}', , 1);								-- remove it from source template
	end
	
	for _, t in ipairs (params) do												-- spin through the templates in params
		anchor_id = sfnref_get (t);												-- attempt to decode  and 
		if anchor_id then														-- nil when not  or 
			list_add (anchor_id, anchor_id_list, true);							-- add anchor ID to the list
		end
	end
	
	template = template:gsub ('|%s*|', '|');									-- when pipe follows pipe with or without white space, remove extraneous pipe
	template = template:gsub ('^|', ):gsub('|$', );							-- remove extraneous leading and trailing pipes
	params = mw.text.split (template, '%s*|%s*');								-- split at the pipe and remove extraneous space characters
	
	for _, t in ipairs (params) do												-- spin through the anchor IDs
		anchor_id = mw.text.trim (t);											-- trim white space
		if  ~= anchor_id then													-- should always have something
			list_add (anchor_id, anchor_id_list, true);							-- add anchor ID to the list
		end
	end
end
------------------------------------------------------------------------
makes a list of templates use in the article.
local function template_list_add (template)
	local template = template:match ('+)') then
				anchor_id = 'Reference-' .. template:match ('|%s*id%s*=(}+)');	-- plain-text
			else
				anchor_id = nil;												-- no matches, ensure that anchor_id has no value
			end
			
			if anchor_id then
				list_add (anchor_id, anchor_id_list, true);
			end
		elseif data.known_templates_anchor template_name then
			anchor_id_make_anchor (template, anchor_id_list);					-- extract anchor ids from this template if any
		
		elseif data.known_templates_sfn_whitelist template_name then
			template = template:gsub ('^', ):gsub ('$', , 1);		-- remove outer  and template name
			template = mw.text.trim (template, '%s|');							-- trim leading trailing white space and pipes
			template = mw.text.split (template, '%s*|%s*');						-- make a table of the template's parameters
			for _, anchor_id in ipairs (template) do							-- spin through this template's parameter
				if  ~= anchor_id and not article_whitelistanchor_id then
					anchor_id = mw.uri.anchorEncode (anchor_id)
					article_whitelistanchor_id = 1;							-- add to the whitelist
				end
			end
		
		elseif template_name and whitelist.wrapper_templatestemplate_name then
			anchor_id = anchor_id_make_wrapper (template);						-- extract an anchor id from this template if possible
			list_add (anchor_id, anchor_id_list, true);
		elseif template_name and template_name:match ('^Citea') then			-- not known, not known wrapper; last gasp, try as cs1-like
			anchor_id = anchor_id_make_cs12 (template);							-- extract an anchor id from this template if possible
			list_add (anchor_id, anchor_id_list, true);
		end
		tstart, tend = Article_content:find (find_pattern, tend);				-- search for another template; begin at end of last search
	end
mw.logObject (anchor_id_list, 'anchor_id_list');
mw.logObject (template_list, 'template_list');
mw.logObject (article_whitelist, 'article_whitelist');
	return anchor_id_list;
end
----------------------------------------------------------------------------
return {
	anchor_id_list = anchor_id_list_make(),										-- table of anchor ids available in this article
	article_whitelist = article_whitelist,										-- table of anchor ids with false-positive error message to be suppressed
	template_list = template_list,												-- table of templates used in this article
	}