Module:Footnotes/sandbox
Appearance
![]() | This is the module sandbox page for Module:Footnotes (diff). See also the companion subpage for test cases (run). |
![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
![]() | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
![]() | This Lua module is used on approximately 329,000 pages, or roughly 1% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
![]() | This module depends on the following other modules: |
Implements {{sfn}}, {{harvard citation}}, and variants of those templates.
For an explanation of generated error messages and how to alter their appearance, see here.
For end-to-end tests of the sandbox version of this module, see Template:Sfnp/test 1, Template:Sfnp/test 2 and Template:Sfnp/test 3.
Tracking categories
[edit]require('strict');localgetArgs=require('Module:Arguments').getArgs;--[[--------------------------< A R G S _ D E F A U L T >------------------------------------------------------a table to specify initial values.]]localargs_default={group='',bracket_left='',bracket_right='',bracket_year_left='',bracket_year_right='',postscript='',page='',pages='',location='',page_sep=", p. ",pages_sep=", pp. ",ref='',template='harv',-- if template name not provided in {{#invoke:}} use this};--[[--------------------------< T A R G E T _ C H E C K >------------------------------------------------------look for anchor_id (CITEREF name-list and year or text from |ref=) in anchor_id_listthe 'no target' error may be suppressed with |ignore-err=yes when target cannot be found because target is insidea template that wraps another template; 'multiple targets' error may not be suppressed]]localfunctiontarget_check(anchor_id,args)localnamespace=mw.title.getCurrentTitle().namespace;localanchor_id_list_module=mw.loadData('Module:Footnotes/anchor_id_list');localanchor_id_list=anchor_id_list_module.anchor_id_list;localarticle_whitelist=anchor_id_list_module.article_whitelist;localtemplate_list=anchor_id_list_module.template_list;localciteref_patterns=anchor_id_list_module.citeref_patternslocalwhitelist_module=mw.loadData('Module:Footnotes/whitelist');localwhitelist=whitelist_module.whitelist;localtally=anchor_id_list[anchor_id];-- nil when anchor_id not in list; else a tallylocalmsg;localcategory;ifnottallythenifargs.ignorethenreturn'';-- if ignore is true then no message, no categoryendifarticle_whitelistandarticle_whitelist[anchor_id]then-- if an article-local whitelist and anchor ID is in itreturn'';-- doneendlocalwl_anchor_id=anchor_id;-- copy to be modified to index into the whitelistifargs.yearthen-- for anchor IDs created by this template (not in |ref=) that have a dateifargs.year:match('%d%l$')or-- use the date value to determine if we should remove the disambiguatorargs.year:match('n%.d%.%l$')orargs.year:match('nd%l$')thenwl_anchor_id=wl_anchor_id:gsub('%l$','');-- remove the disambiguatorendendlocalt_tbl=whitelist[wl_anchor_id];-- get list of templates associated with this anchor IDift_tblthen-- when anchor ID not whitelisted t_tbl is nilfor_,tinipairs(t_tbl)do-- spin through the list of templates associated with this anchor IDiftemplate_list[t]then-- if associated template is found in the list of templates in the articlereturn'';-- anchor ID is whitlisted and article has matching template so no errorendendendfor_,patterninipairs(citeref_patterns)do-- load patterns for wrapper templates on this pageifanchor_id:match(pattern)then-- spin through the special patterns and try to matchreturn''endendmsg='no target: '..anchor_id;-- anchor_id not foundmw.log(msg)ifnamespace==10andnotargs.showthen-- do not generate error message in template namespacereturn''endcategory='[[Category:Harv and Sfn no-target errors]]';elseif1<tallythenmsg='multiple targets ('..tally..'×): '..anchor_id;-- more than one anchor_id in this articlemw.log(msg)ifnamespace==10andnotargs.showthen-- do not generate error message in template namespacereturn''endcategory=0==namespaceand'[[Category:Harv and Sfn multiple-target errors]]'or'';-- only categorize in article spacereturn'<span class="error harv-error" style="display: inline; font-size:100%"> '..args.template..' error: '..msg..' ([[:Category:Harv and Sfn template errors|help]])</span>'..category;end-- category = 0 == namespace and '[[Category:Harv and Sfn template errors]]' or ''; -- only categorize in article spacecategory=0==namespaceandcategoryor'';-- only categorize in article space-- display based on args.show (no display by default)localdisplay=args.showand'inline'or'none'returnmsgand'<span class="error harv-error" style="display: '..display..'; font-size:100%"> '..args.template..' error: '..msg..' ([[:Category:Harv and Sfn template errors|help]])</span>'..categoryor'';end--[[--------------------------< I S _ Y E A R >----------------------------------------------------------------evaluates param to see if it is one of these forms with or without lowercase letter disambiguator: YYYY n.d. n.d.- (This will be suffixed by a letter when using author-date citations for the same author.) nd c. YYYY YYYY–YYYY (separator is endash) YYYY–YY (separator is endash)return true when param has a recognized form; false else]]localpatterns_date={'^%d%d%d%d?%l?$','^n%.d%.%l?$','^n%.d%.%-%l$','^nd%l?$','^c%. %d%d%d%d?%l?$','^%d%d%d%d–%d%d%d%d%l?$','^%d%d%d%d–%d%d%l?$',}localfunctionis_year(param,args)args.year='';-- used for harv error; for_,patterninipairs(patterns_date)doifmw.ustring.match(param,pattern)thenargs.year=param;-- used for harv error; returntrue;endendend--[[--------------------------< C O R E >----------------------------------------------------------------------returns an anchor link (CITEREF) formed from one to four author names, year, and insource location (|p=, |pp=, loc=)]]localfunctioncore(args)localresult;localerr_msg=''ifargs.P5~=''thenifis_year(args.P5,args)thenresult=table.concat({args.P1,' et al. ',args.bracket_year_left,args.P5,args.bracket_year_right});elseargs.P5='';-- when P5 not a year don't include in anchorresult=table.concat({args.P1,' et al.'});-- and don't render itendelseifargs.P4~=''thenifis_year(args.P4,args)thenresult=table.concat({args.P1,', ',args.P2,' & ',args.P3,' ',args.bracket_year_left,args.P4,args.bracket_year_right});-- three names and a yearelseresult=table.concat({args.P1,' et al.'});-- four namesendelseifargs.P3~=''thenifis_year(args.P3,args)thenresult=table.concat({args.P1,' & ',args.P2,' ',args.bracket_year_left,args.P3,args.bracket_year_right});-- two names and a yearelseresult=table.concat({args.P1,', ',args.P2,' ',' & ',args.P3});-- three namesendelseifargs.P2~=''thenifis_year(args.P2,args)thenresult=table.concat({args.P1,' ',args.bracket_year_left,args.P2,args.bracket_year_right});-- one name and yearelseresult=table.concat({args.P1,' & ',args.P2});-- two namesendelseresult=args.P1;-- one nameend-- when author-date result ends with a dot (typically when the last positional parameter holds 'n.d.')-- and when no in-source location (no |p=, |pp=, or |loc=)-- and when the first or only character in args.postscript is a dot-- remove the author-date result trailing dot-- the author-date result trailing dot will be replaced later with the content of args.postscript (usually a dot)if('.'==result:sub(-1))and('.'==args.postscript:sub(1))and(''==args.page)and(''==args.pages)and(''==args.location)thenresult=result:gsub('%.$','');endifargs.ref~='none'thenlocalanchor_id;ifargs.ref~=''thenanchor_id=mw.uri.anchorEncode(args.ref);err_msg=target_check(anchor_id,args);result=table.concat({'[[#',anchor_id,'|',result,']]'});elseanchor_id=mw.uri.anchorEncode(table.concat({'CITEREF',args.P1,args.P2,args.P3,args.P4,args.P5}));err_msg=target_check(anchor_id,args);result=table.concat({'[[#',anchor_id,'|',result,']]'});endendifargs.page~=''thenresult=table.concat({result,args.page_sep,args.page});elseifargs.pages~=''thenresult=table.concat({result,args.pages_sep,args.pages});endifargs.location~=''thenresult=table.concat({result,', ',args.location});endresult=table.concat({args.bracket_left,result,args.bracket_right,args.postscript}):gsub('%s+',' ');-- strip redundant spacesreturnresult..err_msg;end--[[--------------------------< H Y P H E N _ T O _ D A S H >--------------------------------------------------Converts a hyphen to a dash under certain conditions. The hyphen must separatelike items; unlike items are returned unmodified. These forms are modified: letter - letter (A - B) digit - digit (4-5) digit separator digit - digit separator digit (4.1-4.5 or 4-1-4-5) letterdigit - letterdigit (A1-A5) (an optional separator between letter and digit is supported – a.1-a.5 or a-1-a-5) digitletter - digitletter (5a - 5d) (an optional separator between letter and digit is supported – 5.a-5.d or 5-a-5-d)any other forms are returned unmodified.str may be a comma- or semicolon-separated listThis code copied from Module:Citation/CS1. The only modification is to require Module:Citation/CS1/Utilitiesso that it has access to the functions is_set() and has_accept_as_written()]]localfunctionhyphen_to_dash(str)localutilities=require('Module:Citation/CS1/Utilities');-- only modification so that this function has access to is_set() and has_accept_as_written()ifnotutilities.is_set(str)thenreturnstr;endlocalaccept;-- Booleanstr=str:gsub('&[nm]dash;',{['–']='–',['—']='—'});-- replace — and – entities with their characters; semicolon mucks up the text.splitstr=str:gsub('-','-');-- replace HTML numeric entity with hyphen characterstr=str:gsub(' ',' ');-- replace entity with generic keyboard space characterlocalout={};locallist=mw.text.split(str,'%s*[,;]%s*');-- split str at comma or semicolon separators if there are anyfor_,iteminipairs(list)do-- for each item in the listitem,accept=utilities.has_accept_as_written(item);-- remove accept-this-as-written markup when it wraps all of itemifnotacceptandmw.ustring.match(item,'^%w*[%.%-]?%w+%s*[%-–—]%s*%w*[%.%-]?%w+$')then-- if a hyphenated range or has endash or emdash separatorsifitem:match('^%a+[%.%-]?%d+%s*%-%s*%a+[%.%-]?%d+$')or-- letterdigit hyphen letterdigit (optional separator between letter and digit)item:match('^%d+[%.%-]?%a+%s*%-%s*%d+[%.%-]?%a+$')or-- digitletter hyphen digitletter (optional separator between digit and letter)item:match('^%d+[%.%-]%d+%s*%-%s*%d+[%.%-]%d+$')or-- digit separator digit hyphen digit separator digititem:match('^%d+%s*%-%s*%d+$')or-- digit hyphen digititem:match('^%a+%s*%-%s*%a+$')then-- letter hyphen letteritem=item:gsub('(%w*[%.%-]?%w+)%s*%-%s*(%w*[%.%-]?%w+)','%1–%2');-- replace hyphen, remove extraneous space characterselseitem=mw.ustring.gsub(item,'%s*[–—]%s*','–');-- for endash or emdash separated ranges, replace em with en, remove extraneous whitespaceendendtable.insert(out,item);-- add the (possibly modified) item to the output tableendlocaltemp_str='';-- concatenate the output table into a comma separated stringtemp_str,accept=utilities.has_accept_as_written(table.concat(out,', '));-- remove accept-this-as-written markup when it wraps all of concatenated outifacceptthentemp_str=utilities.has_accept_as_written(str);-- when global markup removed, return original str; do it this way to suppress boolean second return valuereturntemp_str;elsereturntemp_str;-- else, return assembled temp_strendend--[[--------------------------< A R G S _ F E T C H >---------------------------------------------------------Because all of the templates share a common set of parameters, a single common function to fetch those parametersfrom frame and parent frame.]]localfunctionargs_fetch(frame,ps)localargs=args_default;-- create a copy of the default tablelocalpframe=frame:getParent();-- point to the template's parameter tablefork,vinpairs(frame.args)do-- override defaults with values provided in the #invoke: if anyargs[k]=v;endargs.postscript=pframe.args.postscriptorpframe.args.psorps;if'none'==args.postscriptthenargs.postscript='';endargs.group=pframe.args.groupor'';args.page=pframe.args.porpframe.args.pageor'';args.pages=pframe.args.pporpframe.args.pagesor'';args.pages=(''~=args.pages)andhyphen_to_dash(args.pages)or'';args.location=pframe.args.atorpframe.args.locor'';args.ref=pframe.args.reforpframe.args.Refor'';args.ignore=('yes'==pframe.args['ignore-false-positive'])or('yes'==pframe.args['ignore-err']);fori,vinipairs({'P1','P2','P3','P4','P5'})do-- loop through the five positional parameters and trim if set else empty stringargs[v]=(pframe.args[i]andmw.text.trim(pframe.args[i]))or'';endifargs.P5andnotis_year(args.P5,args)thenlocali=6;-- initialize the indexer to the sixth positional parameterwhilepframe.args[i]do-- in case there are too many authors loop through the authors looking for a yearlocalv=mw.text.trim(pframe.args[i]);-- trimifis_year(v,args)then-- if a yearargs.P5=v;-- overwrite whatever was in args.P5 with yearbreak;-- and abandon the searchendi=i+1;-- bump the indexerendendreturnargs;end--[[--------------------------< H A R V A R D _ C I T A T I O N >----------------------------------------------common entry point for: {{harvard citation}} aka {{harv}} {{Harvard citation no brackets}} aka {{harvnb}} {{harvcol}} {{harvcolnb}} {{harvcoltxt}} {{Harvard citation text}} aka {{harvtxt}} {{Harvp}}Distinguishing features (brackets and page separators) are specified in this module's {{#invoke}} in the respective templates.]]localfunctionharvard_citation(frame)localargs=args_fetch(frame,'');-- get the template and invoke parameters; default postscript is empty stringreturncore(args);end--[[--------------------------< S T R I P _ U R L >------------------------------------------------------------used by sfn() and sfnm(). This function fixes an issue with reference tooltip gadget where the tooltip is not displayedwhen an insource locator (|p=, |pp=, |loc=) has an external wikilink that contains a # characterstrip uri-reserved characters from urls in |p=, |pp-, and |loc= parameters The researved characters are: !#$&'()*+,/:;=?@[]]]localfunctionstrip_url(pages)localescaped_uri;ifnotpagesor(''==pages)thenreturnpages;endforuriinpages:gmatch('%[(%a[%w%+%.%-]*://%S+)')do-- for each external link get the uriescaped_uri=uri:gsub("([%(%)%.%%%+%-%*%?%[%^%$%]])","%%%1");-- save a copy with lua pattern characters escapeduri=uri:gsub("[!#%$&'%(%)%*%+,/:;=%?@%[%]%.%%]",'');-- remove reserved characters and '%' because '%20' (space character) is a lua 'invalid capture index'pages=pages:gsub(escaped_uri,uri,1);-- replace original uri with the stripped versionendreturnpages;end--[[--------------------------< S F N >------------------------------------------------------------------------entry point for {{sfn}} and {{sfnp}}]]localfunctionsfn(frame)localargs=args_fetch(frame,'.');-- get the template and invoke parameters; default postscript is a dotlocalresult=core(args);-- go make a CITEREF anchor-- put it all together and then strip redundant spaceslocalname=table.concat({'FOOTNOTE',args.P1,args.P2,args.P3,args.P4,args.P5,strip_url(args.page),strip_url(args.pages),strip_url(args.location)}):gsub('%s+',' ');returnframe:extensionTag({name='ref',args={group=args.group,name=name},content=result});end--[[--------------------------< S F N M >----------------------------------------------------------------------common entry point for {{sfnm}} and {{sfnmp}}Distinguishing features (brackets) are specified in this module's {{#invoke}} in the respective templates.]]localfunctionsfnm(frame)localargs=args_default;-- create a copy of the default tablelocalpframe=frame:getParent();-- point to the template's parameter tablelocaln=1;-- index of source; this is the 'n' in na1, ny, etclocalfirst_pnum=1;-- first of a pair of positional parameterslocalsecond_pnum=2;-- second of a pair of positional parameterslocallast_ps=0;-- index of the last source with |nps= setlocallast_index=0;-- index of the last source; these used to determine which of |ps= or |nps= will terminate the whole renderinglocalout={};-- table to hold rendered sourceslocalfootnote={'FOOTNOTE'};-- all author, date, insource location stuff becomes part of the reference's footnote id; added as we gofork,vinpairs(frame.args)do-- override defaults with values provided in the #invoke: if anyargs[k]=v;endwhiletruedoifnotpframe.args[table.concat({n,'a1'})]andnotpframe.args[first_pnum]thenbreak;-- no na1 or matching positional parameter so doneendifpframe.args[table.concat({n,'a1'})]then-- does this source use named parameters?for_,vinipairs({'P1','P2','P3','P4','P5'})do-- initialize for this sourceargs[v]='';endfori,vinipairs({'P1','P2','P3','P4','P5'})do-- extract author and year parameters for this sourceargs[v]=pframe.args[table.concat({n,'a',i})]or'';-- attempt to assign author nameif''==args[v]then-- when there wasn't an author nameargs[v]=pframe.args[table.concat({n,'y'})]or'';-- attempt to assign yearbreak;-- done with author/date for this sourceendendelse-- this source uses positional parametersargs.P1=mw.text.trim(pframe.args[first_pnum]);-- yes, only one author supportedargs.P2=(pframe.args[second_pnum]andmw.text.trim(pframe.args[second_pnum]))or'';-- when positional author, year must also be positionalfor_,vinipairs({'P3','P4','P5'})do-- blank the rest of these for this sourceargs[v]='';endfirst_pnum=first_pnum+2;-- source must use positional author and positional yearsecond_pnum=first_pnum+1;-- bump these for possible next positional sourceendargs.postscript=pframe.args[table.concat({n,'ps'})]or'';if'none'==args.postscriptthen-- this for compatibility with other footnote templates; does nothingargs.postscript='';endargs.group=pframe.args.groupor'';-- reference groupargs.ref=pframe.args[table.concat({n,'ref'})]or'';-- alternate reference for this sourceargs.page=pframe.args[table.concat({n,'p'})]or'';-- insource locations for this sourceargs.pages=pframe.args[table.concat({n,'pp'})]or'';args.pages=(''~=args.pages)andhyphen_to_dash(args.pages)or'';args.location=pframe.args[table.concat({n,'loc'})]orpframe.args[table.concat({n,'at'})]or'';args.ignore=('yes'==pframe.args[table.concat({n,'ignore-false-positive'})])or('yes'==pframe.args[table.concat({n,'ignore-err'})]);table.insert(out,core(args));-- save the rendering of this sourcefork,vinipairs({'P1','P2','P3','P4','P5'})do-- create the FOOTNOTE idif''~=args[v]thentable.insert(footnote,args[v]);endendfork,vinipairs({'page','pages','location'})do-- these done separately so that we can strip uri-reserved characters from extlinked page numbers if''~=args[v]thentable.insert(footnote,strip_url(args[v]))endendlast_index=n;-- flags used to select terminal postscript from nps or from end_psif''~=args.postscriptthenlast_ps=n;endn=n+1;-- bump for the next oneendlocalname=table.concat(footnote):gsub('%s+',' ');-- put the footnote together and strip redundant spaceargs.end_ps=pframe.args.postscriptorpframe.args.psor'.';-- this is the postscript for the whole not for the individual sourcesif'none'==args.end_psthen-- not an original sfnm parameter value; added for compatibility with other footnote templatesargs.end_ps='';endlocalresult=table.concat({table.concat(out,'; '),(last_index==last_ps)and''orargs.end_ps});returnframe:extensionTag({name='ref',args={group=args.group,name=name},content=result});end--[[--------------------------< S F N R E F >------------------------------------------------------------------implements {{sfnref}}]]localfunctionsfnref(frame)localargs=getArgs(frame);localout={};fori=1,5do-- get the first five args if there are five argsifargs[i]thenout[i]=args[i];elsebreak;-- less than 5 args break outendendif5==#outthen-- when we have seen five args there may bemorelocali=6;-- initialize the indexer to the sixth positional parameterwhileargs[i]do-- in case there are too many authors loop through the authors looking for a yearifis_year(args[i],args)then-- if a yearout[5]=args[i];-- overwrite whatever was in args[5] with yearbreak;-- and abandon the searchendi=i+1;-- bump the indexerendendreturnmw.uri.anchorEncode('CITEREF'..table.concat(out));end--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------]]return{harvard_citation=harvard_citation,sfn=sfn,sfnm=sfnm,sfnref=sfnref,target_check=target_check,};