Module:Citation/CS1: Difference between revisions

    m>Trappist the monk
    (Synch from sandbox;)
    m>Trappist the monk
    (Synch from sandbox;)
    Line 11: Line 11:
    ]]
    ]]
    local dates, year_date_check -- functions in Module:Citation/CS1/Date_validation
    local dates, year_date_check -- functions in Module:Citation/CS1/Date_validation
    local cfg = {}; -- table of configuration tables that are defined in Module:Citation/CS1/Configuration
    local whitelist = {}; -- talbe of tables listing valid template parameter names; defined in Module:Citation/CS1/Whitelist


    --[[--------------------------< I S _ S E T >------------------------------------------------------------------
    --[[--------------------------< I S _ S E T >------------------------------------------------------------------
    Line 111: Line 114:
    return error_comment( message, error_state.hidden );
    return error_comment( message, error_state.hidden );
    end
    --[[--------------------------< A D D _ M A I N T _ C A T >------------------------------------------------------
    Adds a category to z.maintenance_cats using names from the configuration file with additional text if any.
    ]]
    local function add_maint_cat (key, arguments)
    table.insert( z.maintenance_cats, substitute (cfg.maint_cats [key], arguments)); -- make name then add to table
    end
    --[[--------------------------< A D D _ P R O P _ C A T >--------------------------------------------------------
    Adds a category to z.properties_cats using names from the configuration file with additional text if any.
    ]]
    local function add_prop_cat (key, arguments)
    table.insert( z.properties_cats, substitute (cfg.prop_cats [key], arguments)); -- make name then add to table
    end
    end


    Line 302: Line 325:
    -- is prefix one of these language codes?
    -- is prefix one of these language codes?
    if in_array (lang, {'ar', 'bg', 'bs', 'dv', 'el', 'fa', 'hy', 'ja', 'ka', 'ko', 'ku', 'he', 'ps', 'ru', 'sd', 'sr', 'th', 'uk', 'ug', 'yi', 'zh'}) then
    if in_array (lang, {'ar', 'bg', 'bs', 'dv', 'el', 'fa', 'hy', 'ja', 'ka', 'ko', 'ku', 'he', 'ps', 'ru', 'sd', 'sr', 'th', 'uk', 'ug', 'yi', 'zh'}) then
    table.insert( z.properties_cats, 'CS1 uses ' .. name .. '-language script ('..lang..')'); -- categorize in language-specific categories
    add_prop_cat ('script_name', {name, lang})
    else
    else
    table.insert( z.properties_cats, 'CS1 uses foreign language script'); -- use this category as a catchall until language-specific category is available
    add_prop_cat ('script')
    end
    end
    lang = ' lang="' .. lang .. '" '; -- convert prefix into a lang attribute
    lang = ' lang="' .. lang .. '" '; -- convert prefix into a lang attribute
    Line 354: Line 377:
    end
    end
    end
    end
    --[[--------------------------< S E L E C T _ O N E >----------------------------------------------------------
    --[[--------------------------< S E L E C T _ O N E >----------------------------------------------------------


    Line 454: Line 478:
    can be transparently aliased to single internal variable.
    can be transparently aliased to single internal variable.
    ]]
    ]]
    local function argument_wrapper( args )
    local function argument_wrapper( args )
    local origin = {};
    local origin = {};
    Line 504: Line 529:
    nil - unsupported parameters
    nil - unsupported parameters
    ]]
    ]]
    local function validate( name )
    local function validate( name )
    local name = tostring( name );
    local name = tostring( name );
    Line 572: Line 598:
    local function is_valid_isxn (isxn_str, len)
    local function is_valid_isxn (isxn_str, len)
    local temp = 0;
    local temp = 0;
    isxn_str = { isxn_str:byte(1, len) }; -- make a table of bytes
    isxn_str = { isxn_str:byte(1, len) }; -- make a table of byte values '0' → 0x30 .. '9'  → 0x39, 'X' → 0x58
    len = len+1; -- adjust to be a loop counter
    len = len+1; -- adjust to be a loop counter
    for i, v in ipairs( isxn_str ) do -- loop through all of the bytes and calculate the checksum
    for i, v in ipairs( isxn_str ) do -- loop through all of the bytes and calculate the checksum
    if v == string.byte( "X" ) then -- if checkdigit is X
    if v == string.byte( "X" ) then -- if checkdigit is X (compares the byte value of 'X' which is 0x58)
    temp = temp + 10*( len - i ); -- it represents 10 decimal
    temp = temp + 10*( len - i ); -- it represents 10 decimal
    else
    else
    Line 674: Line 700:
    if id:match("^%d%d%d%d%d%d%d%d%d[%dX]$") then -- if 10-digit numeric (or 9 digits with terminal X)
    if id:match("^%d%d%d%d%d%d%d%d%d[%dX]$") then -- if 10-digit numeric (or 9 digits with terminal X)
    if check_isbn( id ) then -- see if asin value is isbn10
    if check_isbn( id ) then -- see if asin value is isbn10
    table.insert( z.maintenance_cats, 'CS1 maint: ASIN uses ISBN'); -- add to maint category
    add_maint_cat ('ASIN');
    elseif not is_set (err_cat) then
    elseif not is_set (err_cat) then
    err_cat =  ' ' .. set_error ('bad_asin'); -- asin is not isbn10
    err_cat =  ' ' .. set_error ('bad_asin'); -- asin is not isbn10
    Line 724: Line 750:
    ]]
    ]]


    local function arxiv (id)
    local function arxiv (id, class)
    local handler = cfg.id_handlers['ARXIV'];
    local handler = cfg.id_handlers['ARXIV'];
    local year, month, version;
    local year, month, version;
    local err_cat = ""
    local err_cat = '';
    local text;
    if id:match("^%a[%a%.%-]+/[90]%d[01]%d%d%d%d$") or id:match("^%a[%a%.%-]+/[90]%d[01]%d%d%d%dv%d+$") then -- test for the 9108-0703 format w/ & w/o version
    if id:match("^%a[%a%.%-]+/[90]%d[01]%d%d%d%d$") or id:match("^%a[%a%.%-]+/[90]%d[01]%d%d%d%dv%d+$") then -- test for the 9108-0703 format w/ & w/o version
    Line 756: Line 783:
    end
    end


    return external_link_id({link = handler.link, label = handler.label,
    text = external_link_id({link = handler.link, label = handler.label,
    prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode}) .. err_cat;
    prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode}) .. err_cat;
    if is_set (class) then
    class = ' [[' .. '//arxiv.org/archive/' .. class .. ' ' .. class .. ']]'; -- external link within square brackets, not wikilink
    else
    class = ''; -- empty string for concatenation
    end
    return text .. class;
    end
    end


    --[[
    --[[
    Line 804: Line 838:


    ]]
    ]]
    local function lccn(lccn)
    local function lccn(lccn)
    local handler = cfg.id_handlers['LCCN'];
    local handler = cfg.id_handlers['LCCN'];
    Line 850: Line 885:
    contains only digits and is less than test_limit; the value in local variable test_limit will need to be updated periodically as more PMIDs are issued.
    contains only digits and is less than test_limit; the value in local variable test_limit will need to be updated periodically as more PMIDs are issued.
    ]]
    ]]
    local function pmid(id)
    local function pmid(id)
    local test_limit = 30000000; -- update this value as PMIDs approach
    local test_limit = 30000000; -- update this value as PMIDs approach
    Line 868: Line 904:
    end
    end


    --[[
    --[[--------------------------< I S _ E M B A R G O E D >------------------------------------------------------
     
    Determines if a PMC identifier's online version is embargoed. Compares the date in |embargo= against today's date.  If embargo date is
    Determines if a PMC identifier's online version is embargoed. Compares the date in |embargo= against today's date.  If embargo date is
    in the future, returns true; otherwise, returns false because the embargo has expired or |embargo= not set in this cite.
    in the future, returns the content of |embargo=; otherwise, returns and empty string because the embargo has expired or because
    |embargo= was not set in this cite.
     
    ]]
    ]]
    local function is_embargoed(embargo)
     
    if is_set(embargo) then
    local function is_embargoed (embargo)
    if is_set (embargo) then
    local lang = mw.getContentLanguage();
    local lang = mw.getContentLanguage();
    local good1, embargo_date, good2, todays_date;
    local good1, embargo_date, good2, todays_date;
    Line 879: Line 919:
    good2, todays_date = pcall( lang.formatDate, lang, 'U' );
    good2, todays_date = pcall( lang.formatDate, lang, 'U' );
    if good1 and good2 and tonumber( embargo_date ) >= tonumber( todays_date ) then --is embargo date is in the future?
    if good1 and good2 then -- if embargo date and today's date are good dates
    return true; -- still embargoed
    if tonumber( embargo_date ) >= tonumber( todays_date ) then -- is embargo date is in the future?
    return embargo; -- still embargoed
    else
    add_maint_cat ('embargo')
    return ''; -- unset because embargo has expired
    end
    end
    end
    end
    end
    return false; -- embargo expired or |embargo= not set
    return ''; -- |embargo= not set return empty string
    end
    end


    --[[
    --[[--------------------------< P M C >------------------------------------------------------------------------
     
    Format a PMC, do simple error checking, and check for embargoed articles.
    Format a PMC, do simple error checking, and check for embargoed articles.


    The embargo parameter takes a date for a value. If the embargo date is in the future
    The embargo parameter takes a date for a value. If the embargo date is in the future the PMC identifier will not
    the PMC identifier will not be linked to the article.  If the embargo specifies a date in the past, or if it is empty or omitted, then
    be linked to the article.  If the embargo date is today or in the past, or if it is empty or omitted, then the
    the PMC identifier is linked to the article through the link at cfg.id_handlers['PMC'].prefix.
    PMC identifier is linked to the article through the link at cfg.id_handlers['PMC'].prefix.
     
    PMC embargo date testing is done in function is_embargoed () which is called earlier because when the citation
    has |pmc=<value> but does not have a |url= then |title= is linked with the PMC link.  Function is_embargoed ()
    returns the embargo date if the PMC article is still embargoed, otherwise it returns an empty string.


    PMCs are sequential numbers beginning at 1 and counting up.  This code checks the PMC to see that it contains only digits and is less
    PMCs are sequential numbers beginning at 1 and counting up.  This code checks the PMC to see that it contains only digits and is less
    than test_limit; the value in local variable test_limit will need to be updated periodically as more PMCs are issued.
    than test_limit; the value in local variable test_limit will need to be updated periodically as more PMCs are issued.
    ]]
    ]]
    local function pmc(id, embargo)
    local function pmc(id, embargo)
    local test_limit = 5000000; -- update this value as PMCs approach
    local test_limit = 5000000; -- update this value as PMCs approach
    Line 912: Line 964:
    end
    end
    if is_embargoed(embargo) then
    if is_set (embargo) then -- is PMC is still embargoed?
    text="[[" .. handler.link .. "|" .. handler.label .. "]]:" .. handler.separator .. id .. err_cat; --still embargoed so no external link
    text="[[" .. handler.link .. "|" .. handler.label .. "]]:" .. handler.separator .. id .. err_cat; -- still embargoed so no external link
    else
    else
    text = external_link_id({link = handler.link, label = handler.label, --no embargo date, ok to link to article
    text = external_link_id({link = handler.link, label = handler.label, -- no embargo date or embargo has expired, ok to link to article
    prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode}) .. err_cat;
    prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode}) .. err_cat;
    end
    end
    Line 1,004: Line 1,056:
    return text
    return text
    end
    end


    --[[--------------------------< S E T _ T I T L E T Y P E >----------------------------------------------------
    --[[--------------------------< S E T _ T I T L E T Y P E >----------------------------------------------------


    This function sets default title types (equivalent to the citation including |type=<default value>) for those citations that have defaults.
    This function sets default title types (equivalent to the citation including |type=<default value>) for those templates that have defaults.
    Also handles the special case where it is desirable to omit the title type from the rendered citation (|type=none).
    Also handles the special case where it is desirable to omit the title type from the rendered citation (|type=none).


    ]]
    ]]
    local function set_titletype(cite_class, title_type)
     
    local function set_titletype (cite_class, title_type)
    if is_set(title_type) then
    if is_set(title_type) then
    if "none" == title_type then
    if "none" == title_type then
    title_type = ""; -- if |type=none then type parameter not displayed
    title_type = ""; -- if |type=none then type parameter not displayed
    end
    end
    return title_type; -- if |type= has been set to any other value use that value
    return title_type; -- if |type= has been set to any other value use that value
    end
    end


    if "AV-media-notes" == cite_class or "DVD-notes" == cite_class then -- if this citation is cite AV media notes or cite DVD notes
    return cfg.title_types [cite_class] or ''; -- set template's default title type; else empty string for concatenation
    return "Media notes"; -- display AV media notes / DVD media notes annotation
    end


    elseif "mailinglist" == cite_class then -- if this citation is cite mailing list
    --[[--------------------------< C L E A N _ I S B N >----------------------------------------------------------
    return "Mailing list"; -- display mailing list annotation


    elseif "map" == cite_class then -- if this citation is cite map
    Removes irrelevant text and dashes from ISBN number
    return "Map"; -- display map annotation
    Similar to that used for Special:BookSources


    elseif "podcast" == cite_class then -- if this citation is cite podcast
    ]]
    return "Podcast"; -- display podcast annotation


    elseif "pressrelease" == cite_class then -- if this citation is cite press release
    return "Press release"; -- display press release annotation
    elseif "report" == cite_class then -- if this citation is cite report
    return "Report"; -- display report annotation
    elseif "techreport" == cite_class then -- if this citation is cite techreport
    return "Technical report"; -- display techreport annotation
    elseif "thesis" == cite_class then -- if this citation is cite thesis (degree option handled after this function returns)
    return "Thesis"; -- display simple thesis annotation (without |degree= modification)
    end
    end
    -- Removes irrelevant text and dashes from ISBN number
    -- Similar to that used for Special:BookSources
    local function clean_isbn( isbn_str )
    local function clean_isbn( isbn_str )
    return isbn_str:gsub( "[^-0-9X]", "" );
    return isbn_str:gsub( "[^-0-9X]", "" );
    Line 1,071: Line 1,104:


    ]]
    ]]
    local function strip_apostrophe_markup (argument)
    local function strip_apostrophe_markup (argument)
    if not is_set (argument) then return argument; end
    if not is_set (argument) then return argument; end
    Line 1,121: Line 1,155:


    ]]
    ]]
    local function get_coins_pages (pages)
    local function get_coins_pages (pages)
    local pattern;
    local pattern;
    Line 1,157: Line 1,192:


    ]]
    ]]
    local function safe_join( tbl, duplicate_char )
    local function safe_join( tbl, duplicate_char )
    --[[
    --[[
    Line 1,228: Line 1,264:
    return str;
    return str;
    end   
    end   


    --[[--------------------------< I S _ G O O D _ V A N C _ N A M E >--------------------------------------------
    --[[--------------------------< I S _ G O O D _ V A N C _ N A M E >--------------------------------------------
    Line 1,235: Line 1,270:
    uses characters that contain diacritical marks, those characters are to converted to the corresponding Latin character.
    uses characters that contain diacritical marks, those characters are to converted to the corresponding Latin character.
    When a name is written using a non-Latin alphabet or logogram, that name is to be transliterated into Latin characters.
    When a name is written using a non-Latin alphabet or logogram, that name is to be transliterated into Latin characters.
    These things are not currently possible in this module so are left to the editor to do. This module can, however, check
    These things are not currently possible in this module so are left to the editor to do.
    the content of |lastn= and |firstn= to see if the names contain non-Latin (non-ASCII) characters and emit an error message
     
    when such characters are located.
    This test allows |first= and |last= names to contain any of the letters defined in the four Unicode Latin character sets
    [http://www.unicode.org/charts/PDF/U0000.pdf C0 Controls and Basic Latin] 0041–005A, 0061–007A
    [http://www.unicode.org/charts/PDF/U0080.pdf C1 Controls and Latin-1 Supplement] 00C0–00D6, 00D8–00F6, 00F8–00FF
    [http://www.unicode.org/charts/PDF/U0100.pdf Latin Extended-A] 0100–017F
    [http://www.unicode.org/charts/PDF/U0180.pdf Latin Extended-B] 0180–01BF, 01C4–024F
     
    |lastn= also allowed to contain hyphens, spaces, and apostrophes. (http://www.ncbi.nlm.nih.gov/books/NBK7271/box/A35029/)
    |firstn= also allowed to contain hyphens, spaces, apostrophes, and periods
     
    At the time of this writing, I had to write the 'if nil == mw.ustring.find ...' test ouside of the code editor and past it here
    because the code editor gets confused between character insertion point and cursor position.


    Allow |lastn= to contain ASCII characters, hyphens, spaces, and apostrophes. (http://www.ncbi.nlm.nih.gov/books/NBK7271/box/A35029/)
    Allow |firstn= to contain ASCII characters, hyphens, spaces, apostrophes, and periods
    ]]
    ]]


    local function is_good_vanc_name (last, first)
    local function is_good_vanc_name (last, first)
    if last:find ("[^%a%-%'%s]") or first:find ("[^%a%-%'%s%.]") then
    if nil == mw.ustring.find (last, "^[A-Za-zÀ-ÖØ-öø-ƿDŽ-ɏ%-%s%']*$") or nil == mw.ustring.find (first, "^[A-Za-zÀ-ÖØ-öø-ƿDŽ-ɏ%-%s%'%.]*$") then
    if true ~= Page_in_vanc_error_cat then -- if we haven't been here before then set a sticky flag
    if true ~= Page_in_vanc_error_cat then -- if we haven't been here before then set a sticky flag
    Page_in_vanc_error_cat=true; -- so that if there are more than one error the category is added only once
    Page_in_vanc_error_cat=true; -- so that if there are more than one error the category is added only once
    Line 1,253: Line 1,296:
    return true;
    return true;
    end
    end


    --[[--------------------------< R E D U C E _ T O _ I N I T I A L S >------------------------------------------
    --[[--------------------------< R E D U C E _ T O _ I N I T I A L S >------------------------------------------
    Line 1,263: Line 1,305:
    Vancouver style requires family rank designations (Jr, II, III, etc) to be rendered as Jr, 2nd, 3rd, etc.  This form is not
    Vancouver style requires family rank designations (Jr, II, III, etc) to be rendered as Jr, 2nd, 3rd, etc.  This form is not
    currently supported by this code so correctly formed names like Smith JL 2nd are converted to Smith J2. See http://www.ncbi.nlm.nih.gov/books/NBK7271/box/A35085/.
    currently supported by this code so correctly formed names like Smith JL 2nd are converted to Smith J2. See http://www.ncbi.nlm.nih.gov/books/NBK7271/box/A35085/.
    This function uses ustring functions because firstname initials may be any of the unicode Latin characters accepted by is_good_vanc_name ().


    ]]
    ]]


    local function reduce_to_initials(first)
    local function reduce_to_initials(first)
    if first:match("^%u%u$") then return first end; -- when first contains just two upper-case letters, nothing to do
    if mw.ustring.match(first, "^%u%u$") then return first end; -- when first contains just two upper-case letters, nothing to do
    local initials = {}
    local initials = {}
    local i = 0; -- counter for number of initials
    local i = 0; -- counter for number of initials
    for word in string.gmatch(first, "[^%s%.%-]+") do -- names separated by spaces, hyphens, or periods
    for word in mw.ustring.gmatch(first, "[^%s%.%-]+") do -- names separated by spaces, hyphens, or periods
    table.insert(initials, string.sub(word,1,1)) -- Vancouver format does not include full stops.
    table.insert(initials, mw.ustring.sub(word,1,1)) -- Vancouver format does not include full stops.
    i = i + 1; -- bump the counter  
    i = i + 1; -- bump the counter  
    if 2 <= i then break; end -- only two initials allowed in Vancouver system; if 2, quit
    if 2 <= i then break; end -- only two initials allowed in Vancouver system; if 2, quit
    Line 1,279: Line 1,323:


    --[[--------------------------< L I S T  _ P E O P L E >-------------------------------------------------------
    --[[--------------------------< L I S T  _ P E O P L E >-------------------------------------------------------
    Formats a list of people (e.g. authors / editors)  
    Formats a list of people (e.g. authors / editors)  
    ]]
    ]]
    local function list_people(control, people, etal)
    local function list_people(control, people, etal)
    local sep;
    local sep;
    Line 1,298: Line 1,345:
    if sep:sub(-1,-1) ~= " " then sep = sep .. " " end
    if sep:sub(-1,-1) ~= " " then sep = sep .. " " end
    if maximum ~= nil and maximum < 1 then return "", 0; end
    if is_set (maximum) and maximum < 1 then return "", 0; end -- returned 0 is for EditorCount; not used for authors
    for i,person in ipairs(people) do
    for i,person in ipairs(people) do
    Line 1,305: Line 1,352:
    local one
    local one
    local sep_one = sep;
    local sep_one = sep;
    if maximum ~= nil and i > maximum then
    if is_set (maximum) and i > maximum then
    etal = true;
    etal = true;
    break;
    break;
    Line 1,358: Line 1,405:


    --[[--------------------------< A N C H O R _ I D >------------------------------------------------------------
    --[[--------------------------< A N C H O R _ I D >------------------------------------------------------------
    Generates a CITEREF anchor ID if we have at least one name or a date.  Otherwise returns an empty string.
    Generates a CITEREF anchor ID if we have at least one name or a date.  Otherwise returns an empty string.


    Line 1,437: Line 1,485:
    if true == etal then
    if true == etal then
    table.insert( z.maintenance_cats, 'CS1 maint: Explicit use of et al.'); -- add to maint category
    add_maint_cat ('etal');
    end
    end
    return names, etal; -- all done, return our list of names
    return names, etal; -- all done, return our list of names
    Line 1,453: Line 1,501:


    --[[--------------------------< B U I L D _ I D _ L I S T >--------------------------------------------------------
    --[[--------------------------< B U I L D _ I D _ L I S T >--------------------------------------------------------
    Takes a table of IDs and turns it into a table of formatted ID outputs.
    Takes a table of IDs and turns it into a table of formatted ID outputs.


    ]]
    ]]
    local function build_id_list( id_list, options )
    local function build_id_list( id_list, options )
    local new_list, handler = {};
    local new_list, handler = {};
    Line 1,474: Line 1,524:
    table.insert( new_list, {handler.label, doi( v, options.DoiBroken ) } );
    table.insert( new_list, {handler.label, doi( v, options.DoiBroken ) } );
    elseif k == 'ARXIV' then
    elseif k == 'ARXIV' then
    table.insert( new_list, {handler.label, arxiv( v ) } );  
    table.insert( new_list, {handler.label, arxiv( v, options.Class ) } );  
    elseif k == 'ASIN' then
    elseif k == 'ASIN' then
    table.insert( new_list, {handler.label, amazon( v, options.ASINTLD ) } );  
    table.insert( new_list, {handler.label, amazon( v, options.ASINTLD ) } );  
    Line 1,515: Line 1,565:
    -- COinS metadata (see <http://ocoins.info/>) allows automated tools to parse
    -- COinS metadata (see <http://ocoins.info/>) allows automated tools to parse
    -- the citation information.
    -- the citation information.
    local function COinS(data)
    local function COinS(data, class)
    if 'table' ~= type(data) or nil == next(data) then
    if 'table' ~= type(data) or nil == next(data) then
    return '';
    return '';
    Line 1,538: Line 1,588:
    elseif is_set(data.Periodical) then
    elseif is_set(data.Periodical) then
    OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:journal";
    OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:journal";
    OCinSoutput["rft.genre"] = "article";
    if 'arxiv' == class then
    OCinSoutput["rft.genre"] = "preprint"; -- cite arxiv
    else
    OCinSoutput["rft.genre"] = "article";
    end
    OCinSoutput["rft.jtitle"] = data.Periodical;
    OCinSoutput["rft.jtitle"] = data.Periodical;
    OCinSoutput["rft.atitle"] = data.Title;
    OCinSoutput["rft.atitle"] = data.Title;
    Line 1,610: Line 1,664:


    Adapted from code taken from Module:Check ISO 639-1.
    Adapted from code taken from Module:Check ISO 639-1.
    ]]
    ]]


    Line 1,647: Line 1,702:
    When |language= contains a valid ISO639-1 code, the page is assigned to the category for that code: Category:Norwegian-language sources (no) if
    When |language= contains a valid ISO639-1 code, the page is assigned to the category for that code: Category:Norwegian-language sources (no) if
    the page is a mainspace page and the ISO639-1 code is not 'en'.  Similarly, if the  parameter is |language=Norwegian, it will be categorized in the same way.
    the page is a mainspace page and the ISO639-1 code is not 'en'.  Similarly, if the  parameter is |language=Norwegian, it will be categorized in the same way.
    This function supports multiple languages in the form |language=nb, French, th where the language names or codes are separated from each other by commas.


    ]]
    ]]


    local function language_parameter (lang, namespace)
    local function language_parameter (lang, namespace)
    local code; -- the ISO639-1 two character code
    local code; -- the ISO639-1 two character code
    local name; -- the language name
    local name; -- the language name
    local test='';
    local language_list = {}; -- table of language names to be rendered
    local names_table = {}; -- table made from the value assigned to |language=
    local unrec_cat = false -- flag so that we only add unrecognized category once
    names_table = mw.text.split (lang, '%s*,%s*'); -- names should be a comma separated list
    for _, lang in ipairs (names_table) do -- reuse lang
    if 0 == namespace and (('en' == lang:lower()) or ('english' == lang:lower())) then
    add_maint_cat ('english');
    end
    if 2 == lang:len() then -- ISO639-1 language code are 2 characters (fetchLanguageName also supports 3 character codes)
    name = mw.language.fetchLanguageName( lang:lower(), "en" ); -- get ISO 639-1 language name if Language is a proper code
    end
    if is_set (name) then -- if Language specified a valid ISO639-1 code
    code = lang:lower(); -- save it
    else
    name, code = get_iso639_code (lang); -- attempt to get code from name (assign name here so that we are sure of proper capitalization)
    end
    if is_set (code) then
    if 'no' == code then name = 'Norwegian' end; -- override wikimedia when code is 'no'
    if 0 == namespace and 'en' ~= code then -- is this page main / article space and English not the language?
    add_prop_cat ('foreign_lang_source', {name, code})
    end
    elseif false == unrec_cat then
    unrec_cat = true; -- only add this category once
    add_maint_cat (unknown_lang);
    end
    if 0 == namespace and (('en' == lang:lower()) or ('english' == lang:lower())) then
    table.insert (language_list, name);
    table.insert (z.maintenance_cats, 'CS1 maint: English language specified'); -- add maintenance category if |language=English or |language=en in article space
    name = ''; -- so we can reuse it
    end
    code = #language_list -- reuse code as number of languages in the list
    if 2 >= code then
    name = table.concat (language_list, ' and ') -- insert '<space>and<space>' between two language names
    elseif 2 < code then
    language_list[code] = 'and ' .. language_list[code]; -- prepend last name with 'and<space>'
    name = table.concat (language_list, ', ') -- and concatenate with '<comma><space>' separators
    end
    end
    return (" " .. wrap_msg ('language', name)); -- wrap with '(in ...)'
    end
    --[[--------------------------< S E T _ C S 1 _ S T Y L E >----------------------------------------------------
    Set style settings for CS1 citation templates. Returns separator and postscript settings
    ]]


    if 2 == lang:len() then -- ISO639-1 language code are 2 characters (fetchLanguageName also supports 3 character codes)
    local function set_cs1_style (ps)
    name = mw.language.fetchLanguageName( lang:lower(), "en" ); -- get ISO 639-1 language name if Language is a proper code
    if not is_set (ps) then -- unless explicitely set to something
    ps = '.'; -- terminate the rendered citation with a period
    end
    end
    return '.', ps; -- separator is a full stop
    end


    if is_set (name) then -- if Language specified a valid ISO639-1 code
    --[[--------------------------< S E T _ C S 2 _ S T Y L E >----------------------------------------------------
    code = lang:lower(); -- save it
     
    else
    Set style settings for CS2 citation templates. Returns separator, postscript, ref settings
    name, code = get_iso639_code (lang); -- attempt to get code from name (assign name here so that we are sure of proper capitalization)
     
    ]]
     
    local function set_cs2_style (ps, ref)
    if not is_set (ps) then -- if |postscript= has not been set, set cs2 default
    ps = ''; -- make sure it isn't nil
    end
    end
     
    if not is_set (ref) then -- if |ref= is not set
    if is_set (code) then
    ref = "harv"; -- set default |ref=harv
    if 'no' == code then name = 'Norwegian' end; -- override wikimedia when code is 'no'
    if 0 == namespace and 'en' ~= code then -- is this page main / article space and English not the language?
    table.insert( z.properties_cats, 'CS1 ' .. name .. '-language sources (' .. code .. ')'); -- in main space and not English: categorize
    end
    else
    table.insert (z.maintenance_cats, 'CS1 maint: Unrecognized language'); -- add maintenance category when |language= does not appear to be ISO 639-1 language
    end
    end
    return (" " .. wrap_msg ('language', name)); -- wrap with '(in ...)'
    return ',', ps, ref; -- separator is a comma
    end
    end


    --[[--------------------------< G E T _ S E T T I N G S _ F R O M _ C I T E _ C L A S S >----------------------
    --[[--------------------------< G E T _ S E T T I N G S _ F R O M _ C I T E _ C L A S S >----------------------
    When |mode= is not set or when its value is invalid, use config.CitationClass and parameter values to establish
    When |mode= is not set or when its value is invalid, use config.CitationClass and parameter values to establish
    rendered style.
    rendered style.
    Line 1,687: Line 1,792:


    local function get_settings_from_cite_class (ps, ref, cite_class)
    local function get_settings_from_cite_class (ps, ref, cite_class)
    local sep;
    local sep;
    if (cite_class == "citation") then -- for citation templates (CS2)
    if (cite_class == "citation") then -- for citation templates (CS2)
    sep = ','; -- set citation separator to its default (comma)
    sep, ps, ref = set_cs2_style (ps, ref);
    if not is_set (ps) then -- if |postscript= has not been set, set cs2 default
    ps = ''; -- make sure it isn't nil
    end
    if not is_set (ref) then -- if |ref= is not set
    ref = "harv"; -- set default |ref=harv
    end
    else -- not a citation template so CS1
    else -- not a citation template so CS1
    sep = '.'; -- set cite xxx separator to its default (period)
    sep, ps = set_cs1_style (ps);
    if not is_set (ps) then -- if |postscript= has not been set
    ps = '.'; -- set cs1 default
    end
    end
    end


    Line 1,714: Line 1,810:


    local function set_style (mode, ps, ref, cite_class)
    local function set_style (mode, ps, ref, cite_class)
    local sep;
    local sep;
    if is_set (mode) then
    if 'cs2' == mode then -- if this template is to be rendered in CS2 (citation) style
    if 'cs2' == mode then -- if this template is to be rendered in CS2 (citation) style
    sep, ps, ref = set_cs2_style (ps, ref);
    sep = ','; -- separate elements with a comma
    elseif 'cs1' == mode then -- if this template is to be rendered in CS1 (cite xxx) style
    if not is_set (ps) then -- unless explicitely set to something
    sep, ps = set_cs1_style (ps);
    ps = ''; -- make sure it isn't nil
    else -- anything but cs1 or cs2
    end
    if is_set (mode) then
    if not is_set (ref) then -- unless explicitely set to something
    ref = 'harv'; -- set so this template renders with CITEREF anchor id
    end
    elseif 'cs1' == mode then -- if this template is to be rendered in CS1 (cite xxx) style
    sep = '.'; -- separate elements with a period
    if not is_set (ps) then -- unless explicitely set to something
    ps = '.'; -- terminate the rendered citation with a period
    end
    else -- anything but cs1 or cs2
    table.insert( z.message_tail, { set_error( 'invalid_param_val', {'mode', mode}, true ) } ); -- add error message
    table.insert( z.message_tail, { set_error( 'invalid_param_val', {'mode', mode}, true ) } ); -- add error message
    sep, ps, ref = get_settings_from_cite_class (ps, ref, cite_class); -- get settings based on the template's CitationClass
    end
    end
    else -- when |mode= empty or omitted
    sep, ps, ref = get_settings_from_cite_class (ps, ref, cite_class); -- get settings based on the template's CitationClass
    sep, ps, ref = get_settings_from_cite_class (ps, ref, cite_class); -- get settings based on the template's CitationClass
    end
    end
    Line 1,743: Line 1,828:
    end
    end


    --[=[-------------------------< I S _ P D F >------------------------------------------------------------------
    Determines if a url has the file extension is one of the pdf file extensions used by [[MediaWiki:Common.css]] when
    applying the pdf icon to external links.
    returns true if file extension is one of the recognized extension, else false
    ]=]
    local function is_pdf (url)
    return url:match ('%.pdf[%?#]?') or url:match ('%.PDF[%?#]?');
    end
    --[[--------------------------< S T Y L E _ F O R M A T >------------------------------------------------------
    Applies css style to |format=, |chapter-format=, etc.  Also emits an error message if the format parameter does
    not have a matching url parameter.  If the format parameter is not set and the url contains a file extension that
    is recognized as a pdf document by MediaWiki's commons.css, this code will set the format parameter to (PDF) with
    the appropriate styling.
    ]]
    local function style_format (format, url, fmt_param, url_param)
    if is_set (format) then
    format = wrap_style ('format', format:upper()); -- force upper case, add leading space, parenthases, resize
    if not is_set (url) then
    format = format .. set_error( 'format_missing_url', {fmt_param, url_param} ); -- add an error message
    end
    elseif is_pdf (url) then -- format is not set so if url is a pdf file then
    format = wrap_style ('format', 'PDF'); -- set format to pdf
    else
    format = ''; -- empty string for concatenation
    end
    return format;
    end
    --[[--------------------------< G E T _ D I S P L A Y _ A U T H O R S _ E D I T O R S >------------------------
    Returns a number that may or may not limit the length of the author or editor name lists.
    When the value assigned to |display-authors= is a number greater than or equal to zero, return the number and
    the previous state of the 'etal' flag (false by default but may have been set to true if the name list contains
    some variant of the text 'et al.').
    When the value assigned to |display-authors= is the keyword 'etal', return a number that is one greater than the
    number of authors in the list and set the 'etal' flag true.  This will cause the list_people() to display all of
    the names in the name list followed by 'et al.'
    In all other cases, returns nil and the previous state of the 'etal' flag.
    ]]
    local function get_display_authors_editors (max, count, list_name, etal)
    if is_set (max) then
    if 'etal' == max:lower():gsub("[ '%.]", '') then -- the :gsub() portion makes 'etal' from a variety of 'et al.' spellings and stylings
    max = count + 1; -- number of authors + 1 so display all author name plus et al.
    etal = true; -- overrides value set by extract_names()
    elseif max:match ('^%d+$') then -- if is a string of numbers
    max = tonumber (max); -- make it a number
    if max >= count and 'authors' == list_name then -- AUTHORS ONLY -- if |display-xxxxors= value greater than or equal to number of authors/editors
    add_maint_cat ('disp_auth_ed', list_name);
    end
    else -- not a valid keyword or number
    table.insert( z.message_tail, { set_error( 'invalid_param_val', {'display-' .. list_name, max}, true ) } ); -- add error message
    max = nil; -- unset
    end
    elseif 'authors' == list_name then -- AUTHORS ONLY need to clear implicit et al category
    max = count + 1; -- number of authors + 1
    end
    return max, etal;
    end


    --[[--------------------------< C I T A T I O N 0 >------------------------------------------------------------
    --[[--------------------------< C I T A T I O N 0 >------------------------------------------------------------
    This is the main function doing the majority of the citation
    This is the main function doing the majority of the citation
    formatting.
    formatting.
    ]]
    ]]
    local function citation0( config, args)
    local function citation0( config, args)
    --[[  
    --[[  
    Line 1,798: Line 1,958:
    local Degree = A['Degree'];
    local Degree = A['Degree'];
    local Docket = A['Docket'];
    local Docket = A['Docket'];
    local ArchiveFormat = A['ArchiveFormat'];
    local ArchiveURL = A['ArchiveURL'];
    local ArchiveURL = A['ArchiveURL'];
    local URL = A['URL']
    local URL = A['URL']
    Line 1,803: Line 1,964:
    local ChapterURL = A['ChapterURL'];
    local ChapterURL = A['ChapterURL'];
    local ChapterURLorigin = A:ORIGIN('ChapterURL'); -- get name of parameter that holds ChapterURL
    local ChapterURLorigin = A:ORIGIN('ChapterURL'); -- get name of parameter that holds ChapterURL
    local ConferenceFormat = A['ConferenceFormat'];
    local ConferenceURL = A['ConferenceURL'];
    local ConferenceURL = A['ConferenceURL'];
    local ConferenceURLorigin = A:ORIGIN('ConferenceURL'); -- get name of parameter that holds ConferenceURL
    local ConferenceURLorigin = A:ORIGIN('ConferenceURL'); -- get name of parameter that holds ConferenceURL
    Line 1,835: Line 1,997:
    local IgnoreISBN = A['IgnoreISBN'];
    local IgnoreISBN = A['IgnoreISBN'];
    local Embargo = A['Embargo'];
    local Embargo = A['Embargo'];
    local Class = A['Class']; -- arxiv class identifier


    local ID_list = extract_ids( args );
    local ID_list = extract_ids( args );
    Line 1,840: Line 2,003:
    local Quote = A['Quote'];
    local Quote = A['Quote'];


    local LayFormat = A['LayFormat'];
    local LayURL = A['LayURL'];
    local LayURL = A['LayURL'];
    local LaySource = A['LaySource'];
    local LaySource = A['LaySource'];
    local Transcript = A['Transcript'];
    local Transcript = A['Transcript'];
    local TranscriptFormat = A['TranscriptFormat'];
    local TranscriptURL = A['TranscriptURL']  
    local TranscriptURL = A['TranscriptURL']  
    local TranscriptURLorigin = A:ORIGIN('TranscriptURL'); -- get name of parameter that holds TranscriptURL
    local TranscriptURLorigin = A:ORIGIN('TranscriptURL'); -- get name of parameter that holds TranscriptURL
    Line 1,867: Line 2,032:
    use_lowercase = ( sepc == ',' ); -- used to control capitalization for certain static text
    use_lowercase = ( sepc == ',' ); -- used to control capitalization for certain static text


    --check this page to see if it is in one of the namespaces that cs1 is not supposed to add to the error categories.
    --check this page to see if it is in one of the namespaces that cs1 is not supposed to add to the error categories
    if not is_set(no_tracking_cats) then -- ignore if we are already not going to categorize this page
    if not is_set (no_tracking_cats) then -- ignore if we are already not going to categorize this page
    if in_array (this_page.nsText, cfg.uncategorized_namespaces) then
    if in_array (this_page.nsText, cfg.uncategorized_namespaces) then
    no_tracking_cats = "true"; -- set no_tracking_cats
    no_tracking_cats = "true"; -- set no_tracking_cats
    end
    for _,v in ipairs (cfg.uncategorized_subpages) do -- cycle through page name patterns
    if this_page.text:match (v) then -- test page name against each pattern
    no_tracking_cats = "true"; -- set no_tracking_cats
    break; -- bail out if one is found
    end
    end
    end
    end
    end
    Line 1,979: Line 2,150:
    if (config.CitationClass == "mailinglist") then
    if (config.CitationClass == "mailinglist") then
    Periodical = A ['MailingList'];
    Periodical = A ['MailingList'];
    end
    --Account for the oddity that is {{cite journal}} with |pmc= set and |url= not set
    if config.CitationClass == "journal" and not is_set(URL) and is_set(ID_list['PMC']) then
    if not is_embargoed(Embargo) then
    URL=cfg.id_handlers['PMC'].prefix .. ID_list['PMC']; -- set url to be the same as the PMC external link if not embargoed
    URLorigin = cfg.id_handlers['PMC'].parameters[1]; -- set URLorigin to parameter name for use in error message if citation is missing a |title=
    end
    end
    end


    Line 2,005: Line 2,168:
    URL = '';
    URL = '';
    end
    end
    else
    elseif 'speech' ~= config.CitationClass then
    Conference = ''; -- not cite conference so make sure this is empty string
    Conference = ''; -- not cite conference or cite speech so make sure this is empty string
    end
     
    -- cite map oddities
    local Cartography = "";
    local Scale = "";
    local Sheet = A['Sheet'] or '';
    local Sheets = A['Sheets'] or '';
    if config.CitationClass == "map" then
    Chapter = A['Map'];
    ChapterURL = A['MapURL'];
    TransChapter = A['TransMap'];
    ChapterURLorigin = A:ORIGIN('MapURL');
    ChapterFormat = A['MapFormat'];
    Cartography = A['Cartography'];
    if is_set( Cartography ) then
    Cartography = sepc .. " " .. wrap_msg ('cartography', Cartography, use_lowercase);
    end
    Scale = A['Scale'];
    if is_set( Scale ) then
    Scale = sepc .. " " .. Scale;
    end
    end
    end


    -- Account for the oddity that is {{cite episode}}, before generation of COinS data.
    -- Account for the oddities that are {{cite episode}} and {{cite serial}}, before generation of COinS data.
    --[[ -- {{cite episode}} is not currently supported by this module
    if 'episode' == config.CitationClass or 'serial' == config.CitationClass then
    if config.CitationClass == "episode" then
    local AirDate = A['AirDate'];
    local AirDate = A['AirDate'];
    local Began = A['Began']; -- these two are deprecated because the module understands date ranges
    local Ended = A['Ended'];
    local SeriesLink = A['SeriesLink'];
    local SeriesLink = A['SeriesLink'];
    local Season = A['Season'];
    local SeriesNumber = A['SeriesNumber'];
    local Network = A['Network'];
    local Network = A['Network'];
    local Station = A['Station'];
    local Station = A['Station'];
    local s, n = {}, {};
    local s, n = {}, {};
    local Sep = (first_set(A["SeriesSeparator"], A["Separator"]) or "") .. " ";
     
    -- do common parameters first
    if is_set(Issue) then table.insert(s, cfg.messages["episode"] .. " " .. Issue); Issue = ''; end
    if is_set(Season) then table.insert(s, cfg.messages["season"] .. " " .. Season); end
    if is_set(SeriesNumber) then table.insert(s, cfg.messages["series"] .. " " .. SeriesNumber); end
    if is_set(Network) then table.insert(n, Network); end
    if is_set(Network) then table.insert(n, Network); end
    if is_set(Station) then table.insert(n, Station); end
    if is_set(Station) then table.insert(n, Station); end
    ID = table.concat(n, sepc .. ' ');
    Date = Date or AirDate;
    if not is_set (Date) then -- promote airdate or Began/Ended to date
    Chapter = Title;
    if is_set (AirDate) then
    ChapterLink = TitleLink;
    Date = AirDate;
    TransChapter = TransTitle;
    elseif is_set (Began) then -- deprecated
    Title = Series;
    if Began:match('%s') or Ended:match('%s') then -- so we don't create errors: if either has spaces then
    TitleLink = SeriesLink;
    Date = Began .. ' – ' .. Ended; -- use spaced ndash as separator
    TransTitle = '';
    else
    Date = Began .. '' .. Ended; -- elsewise no spaces
    Series = table.concat(s, Sep);
    end
    ID = table.concat(n, Sep);
    end
    end
    end
    -- end of {{cite episode}} stuff]]
     
    if 'episode' == config.CitationClass then -- handle the oddities that are strictly {{cite episode}}
    local Season = A['Season'];
    local SeriesNumber = A['SeriesNumber'];


    -- legacy: promote concatenation of |month=, and |year= to Date if Date not set; or, promote PublicationDate to Date if neither Date nor Year are set.
    if is_set (Season) and is_set (SeriesNumber) then -- these are mutually exclusive so if both are set
    table.insert( z.message_tail, { set_error( 'redundant_parameters', {wrap_style ('parameter', 'season') .. ' and ' .. wrap_style ('parameter', 'seriesno')}, true ) } ); -- add error message
    SeriesNumber = ''; -- unset; prefer |season= over |seriesno=
    end
    -- assemble a table of parts concatenated later into Series
    if is_set(Season) then table.insert(s, wrap_msg ('season', Season, use_lowercase)); end
    if is_set(SeriesNumber) then table.insert(s, wrap_msg ('series', SeriesNumber, use_lowercase)); end
    if is_set(Issue) then table.insert(s, wrap_msg ('episode', Issue, use_lowercase)); end
    Issue = ''; -- unset because this is not a unique parameter
    Chapter = Title; -- promote title parameters to chapter
    ChapterLink = TitleLink; -- alias episodelink
    TransChapter = TransTitle;
    ChapterURL = URL;
    ChapterURLorigin = A:ORIGIN('URL');
    Title = Series; -- promote series to title
    TitleLink = SeriesLink;
    Series = table.concat(s, sepc .. ' '); -- this is concatenation of season, seriesno, episode number
     
    if is_set (ChapterLink) and not is_set (ChapterURL) then -- link but not URL
    Chapter = '[[' .. ChapterLink .. '|' .. Chapter .. ']]'; -- ok to wikilink
    elseif is_set (ChapterLink) and is_set (ChapterURL) then -- if both are set, URL links episode;
    Series = '[[' .. ChapterLink .. '|' .. Series .. ']]'; -- series links with ChapterLink (episodelink -> TitleLink -> ChapterLink) ugly
    end
    URL = ''; -- unset
    TransTitle = ''; -- unset
    else -- now oddities that are cite serial
    Issue = ''; -- unset because this parameter no longer supported by the citation/core version of cite serial
    Chapter = A['Episode']; -- TODO: make |episode= available to cite episode someday?
    if is_set (Series) and is_set (SeriesLink) then
    Series = '[[' .. SeriesLink .. '|' .. Series .. ']]';
    end
    Series = wrap_style ('italic-title', Series); -- series is italicized
    end
    end
    -- end of {{cite episode}} stuff
     
    -- Account for the oddities that are {{cite arxiv}}, before generation of COinS data.
    if 'arxiv' == config.CitationClass then
    if not is_set (ID_list['ARXIV']) then -- |arxiv= or |eprint= required for cite arxiv
    table.insert( z.message_tail, { set_error( 'arxiv_missing', {}, true ) } ); -- add error message
    end
    if first_set (AccessDate, At, Chapter, Format, Page, Pages, Periodical, PublisherName, URL, -- a crude list of parameters that are not supported by cite arxiv
    ID_list['ASIN'], ID_list['BIBCODE'], ID_list['DOI'], ID_list['ISBN'], ID_list['ISSN'],
    ID_list['JFM'], ID_list['JSTOR'], ID_list['LCCN'], ID_list['MR'], ID_list['OCLC'], ID_list['OL'],
    ID_list['OSTI'], ID_list['PMC'], ID_list['PMID'], ID_list['RFC'], ID_list['SSRN'], ID_list['USENETID'], ID_list['ZBL']) then
    table.insert( z.message_tail, { set_error( 'arxiv_params_not_supported', {}, true ) } ); -- add error message
     
    AccessDate= ''; -- set these to empty string; not supported in cite arXiv
    PublisherName = ''; -- (if the article has been published, use cite journal, or other)
    Chapter = '';
    URL = '';
    Format = '';
    Page = ''; Pages = ''; At = '';
    end
    Periodical = 'arXiv'; -- set to arXiv for COinS; after that, must be set to empty string
    end
     
    -- handle type parameter for those CS1 citations that have default values
     
    if in_array(config.CitationClass, {"AV-media-notes", "DVD-notes", "mailinglist", "map", "podcast", "pressrelease", "report", "techreport", "thesis"}) then
    TitleType = set_titletype (config.CitationClass, TitleType);
    if is_set(Degree) and "Thesis" == TitleType then -- special case for cite thesis
    TitleType = Degree .. " thesis";
    end
    end
     
    if is_set(TitleType) then -- if type parameter is specified
    TitleType = " (" .. TitleType .. ")"; -- display it in parentheses
    end
     
    -- legacy: promote concatenation of |month=, and |year= to Date if Date not set; or, promote PublicationDate to Date if neither Date nor Year are set.
    if not is_set (Date) then
    if not is_set (Date) then
    Date = Year; -- promote Year to Date
    Date = Year; -- promote Year to Date
    Line 2,065: Line 2,323:
    do -- create defined block to contain local variables error_message and mismatch
    do -- create defined block to contain local variables error_message and mismatch
    local error_message = '';
    local error_message = '';
     
    -- AirDate has been promoted to Date so not necessary to check it
    anchor_year, COinS_date, error_message = dates({['accessdate']=AccessDate, ['airdate']=AirDate, ['archivedate']=ArchiveDate, ['date']=Date, ['doi_brokendate']=DoiBroken,
    anchor_year, COinS_date, error_message = dates({['accessdate']=AccessDate, ['archivedate']=ArchiveDate, ['date']=Date, ['doi_brokendate']=DoiBroken,
    ['embargo']=Embargo, ['laydate']=LayDate, ['publicationdate']=PublicationDate, ['year']=Year});
    ['embargo']=Embargo, ['laydate']=LayDate, ['publicationdate']=PublicationDate, ['year']=Year});


    Line 2,077: Line 2,335:
    error_message = error_message .. '&#124;year= / &#124;date= mismatch';
    error_message = error_message .. '&#124;year= / &#124;date= mismatch';
    elseif 1 == mismatch then -- |year= matches year-value in |date=
    elseif 1 == mismatch then -- |year= matches year-value in |date=
    table.insert( z.maintenance_cats, 'CS1 maint: Date and year'); -- add to maint category
    add_maint_cat ('date_year');
    end
    end
    end
    end
    Line 2,085: Line 2,343:
    end
    end
    end -- end of do
    end -- end of do
    -- Account for the oddity that is {{cite journal}} with |pmc= set and |url= not set.  Do this after date check but before COInS.
    -- Here we unset Embargo if PMC not embargoed (|embargo= not set in the citation) or if the embargo time has expired. Otherwise, holds embargo date
    Embargo = is_embargoed (Embargo); --
    if config.CitationClass == "journal" and not is_set(URL) and is_set(ID_list['PMC']) then
    if not is_set (Embargo) then -- if not embargoed or embargo has expired
    URL=cfg.id_handlers['PMC'].prefix .. ID_list['PMC']; -- set url to be the same as the PMC external link if not embargoed
    URLorigin = cfg.id_handlers['PMC'].parameters[1]; -- set URLorigin to parameter name for use in error message if citation is missing a |title=
    end
    end


    -- At this point fields may be nil if they weren't specified in the template use.  We can use that fact.
    -- At this point fields may be nil if they weren't specified in the template use.  We can use that fact.
    Line 2,096: Line 2,365:
    if 'none' == Title and is_set(Periodical) and not (( config.CitationClass == "encyclopaedia" ) or ( config.CitationClass == "citation" and is_set (Encyclopedia))) then -- special case
    if 'none' == Title and is_set(Periodical) and not (( config.CitationClass == "encyclopaedia" ) or ( config.CitationClass == "citation" and is_set (Encyclopedia))) then -- special case
    Title = ''; -- set title to empty string
    Title = ''; -- set title to empty string
    table.insert( z.maintenance_cats, 'CS1 maint: Untitled periodical'); -- add to maint category
    add_maint_cat ('untitled');
    end
    end


    Line 2,114: Line 2,383:
    -- this is the function call to COinS()
    -- this is the function call to COinS()
    local OCinSoutput = COinS{
    local OCinSoutput = COinS({
    ['Periodical'] = Periodical,
    ['Periodical'] = Periodical,
    ['Chapter'] = strip_apostrophe_markup (coins_chapter), -- Chapter stripped of bold / italic wikimarkup
    ['Chapter'] = strip_apostrophe_markup (coins_chapter), -- Chapter stripped of bold / italic wikimarkup
    Line 2,123: Line 2,392:
    ['Volume'] = Volume,
    ['Volume'] = Volume,
    ['Issue'] = Issue,
    ['Issue'] = Issue,
    ['Pages'] = get_coins_pages (first_set(Page, Pages, At)), -- pages stripped of external links
    ['Pages'] = get_coins_pages (first_set(Sheet, Sheets, Page, Pages, At)), -- pages stripped of external links
    ['Edition'] = Edition,
    ['Edition'] = Edition,
    ['PublisherName'] = PublisherName,
    ['PublisherName'] = PublisherName,
    Line 2,130: Line 2,399:
    ['ID_list'] = ID_list,
    ['ID_list'] = ID_list,
    ['RawPage'] = this_page.prefixedText,
    ['RawPage'] = this_page.prefixedText,
    };
    }, config.CitationClass);
     
    -- Account for the oddities that are {{cite arxiv}}, AFTER generation of COinS data.
    if 'arxiv' == config.CitationClass then -- we have set rft.jtitle in COinS to arXiv, now unset so it isn't displayed
    Periodical = ''; -- periodical not allowed in cite arxiv; if article has been published, use cite journal
    end


    -- special case for cite newsgroup.  Do this after COinS because we are modifying Publishername to include some static text
    -- special case for cite newsgroup.  Do this after COinS because we are modifying Publishername to include some static text
    Line 2,144: Line 2,418:
    -- We also add leading spaces and surrounding markup and punctuation to the
    -- We also add leading spaces and surrounding markup and punctuation to the
    -- various parts of the citation, but only when they are non-nil.
    -- various parts of the citation, but only when they are non-nil.
    if not is_set(Authors) then
    do -- do-block to limit scope of LastFirstAuthors
    local Maximum = tonumber( A['DisplayAuthors'] );
    local LastFirstAuthors;
    local Maximum = A['DisplayAuthors'];


    if is_set (Maximum) then
    Maximum , author_etal = get_display_authors_editors (Maximum, #a, 'authors', author_etal);
    if Maximum >= #a then -- if display-authors value greater than or equal to number of authors
    table.insert( z.maintenance_cats, 'CS1 maint: display-authors'); -- add maintenance category because display-authors parameter may be removed
    end
    else
    Maximum = #a + 1; -- number of authors + 1
    end


    local control = {  
    local control = {  
    Line 2,168: Line 2,437:
    end
    end
    Authors = list_people(control, a, author_etal)  
    LastFirstAuthors = list_people(control, a, author_etal);
    end
     
    if is_set (Authors) then
    if is_set (LastFirstAuthors) then -- if both author name styles
    table.insert( z.message_tail, { set_error( 'redundant_parameters', {wrap_style ('parameter', 'authors') .. ' and ' .. wrap_style ('parameter', 'last')}, true ) } ); -- add error message
    Authors = LastFirstAuthors; -- TODO: is this correct or should we use |authors= instead?
    end
    else
    Authors = LastFirstAuthors; -- either an author name list or an empty string
    end
    end -- end of do


    if not is_set(Authors) and is_set(Coauthors) then -- coauthors aren't displayed if one of authors=, authorn=, or lastn= isn't specified
    if not is_set(Authors) and is_set(Coauthors) then -- coauthors aren't displayed if one of authors=, authorn=, or lastn= isn't specified
    Line 2,175: Line 2,453:
    end
    end


    local EditorCount
    local EditorCount; -- used only for choosing {ed.) or (eds.) annotation at end of editor name-list
    if not is_set(Editors) then
    if not is_set(Editors) then
    local Maximum = tonumber( A['DisplayEditors'] );
    local Maximum = A['DisplayEditors'];
     
    Maximum , editor_etal = get_display_authors_editors (Maximum, #e, 'editors', editor_etal);
    -- Preserve old-style implicit et al.
    -- Preserve old-style implicit et al.
    if not is_set(Maximum) and #e == 4 then  
    if not is_set(Maximum) and #e == 4 then  
    Maximum = 3;
    Maximum = 3;
    table.insert( z.message_tail, { set_error('implict_etal_editor', {}, true) } );
    table.insert( z.message_tail, { set_error('implict_etal_editor', {}, true) } );
    elseif not is_set(Maximum) then
    Maximum = #e + 1;
    end
    end


    Line 2,194: Line 2,472:


    Editors, EditorCount = list_people(control, e, editor_etal);
    Editors, EditorCount = list_people(control, e, editor_etal);
    if 1 == EditorCount and (true == editor_etal or 1 < #e) then -- only one editor displayed but includes etal then
    EditorCount = 2; -- spoof to display (eds.) annotation
    end
    else
    else
    EditorCount = 1;
    EditorCount = 1;
    end
    end


    -- cite map oddities
    -- apply |[xx-]format= styling; at the end, these parameters hold correctly styled format annotation,
    local Cartography = "";
    -- an error message if the associated url is not set, or an empty string for concatenation
    local Scale = "";
    ArchiveFormat = style_format (ArchiveFormat, ArchiveURL, 'archive-format', 'archive-url');
    if config.CitationClass == "map" then
    ChapterFormat = style_format (ChapterFormat, ChapterURL, 'chapter-format', 'chapter-url');
    Chapter = A['Map'];
    ConferenceFormat = style_format (ConferenceFormat, ConferenceURL, 'conference-format', 'conference-url');
    ChapterURL = A['MapURL'];
    Format = style_format (Format, URL, 'format', 'url');
    ChapterURLorigin = A:ORIGIN('MapURL');
    LayFormat = style_format (LayFormat, LayURL, 'lay-format', 'lay-url');
    ChapterFormat = A['MapFormat'];
    TranscriptFormat = style_format (TranscriptFormat, TranscriptURL, 'transcript-format', 'transcripturl');
    Cartography = A['Cartography'];
    if is_set( Cartography ) then
    Cartography = sepc .. " " .. wrap_msg ('cartography', Cartography, use_lowercase);
    end
    Scale = A['Scale'];
    if is_set( Scale ) then
    Scale = sepc .. " " .. Scale;
    end
    end
    Format = is_set(Format) and " (" .. Format .. ")" or "";


    if  not is_set(URL) and
    if  not is_set(URL) then --and
    not is_set(ArchiveURL) and
    -- not is_set(ArchiveURL) then --and -- prevents format_missing_url error from registering
    not is_set(ConferenceURL) and -- TODO: keep this here? conference as part of cite web or cite podcast?
    -- not is_set(ConferenceURL) and -- TODO: keep this here? conference as part of cite web or cite podcast?
    not is_set(TranscriptURL) then
    -- not is_set(TranscriptURL) then -- TODO: remove? |transcript-url= and |transcript= has separate test
    -- Test if cite web or cite podcast |url= is missing or empty  
    -- Test if cite web or cite podcast |url= is missing or empty  
    Line 2,233: Line 2,502:
    table.insert( z.message_tail, { set_error( 'accessdate_missing_url', {}, true ) } );
    table.insert( z.message_tail, { set_error( 'accessdate_missing_url', {}, true ) } );
    AccessDate = '';
    AccessDate = '';
    end
    -- Test if format is given without giving a URL
    if is_set(Format) then
    Format = Format .. set_error( 'format_missing_url', {'format', 'url'} );
    end
    end
    end
    end


    local OriginalURL;
    local OriginalURL, OriginalFormat; -- TODO: swap chapter and title here so that archive applies to most specific if both are set?
    DeadURL = DeadURL:lower(); -- used later when assembling archived text
    DeadURL = DeadURL:lower(); -- used later when assembling archived text
    if is_set( ArchiveURL ) then
    if is_set( ArchiveURL ) then
    if is_set (URL) then
    if is_set (URL) then
    OriginalURL = URL; -- save copy of original source URL
    OriginalURL = URL; -- save copy of original source URL
    OriginalFormat = Format; -- and original |format=
    if 'no' ~= DeadURL then -- if URL set then archive-url applies to it
    if 'no' ~= DeadURL then -- if URL set then archive-url applies to it
    URL = ArchiveURL -- swap-in the archive's url
    URL = ArchiveURL -- swap-in the archive's url
    URLorigin = A:ORIGIN('ArchiveURL') -- name of archive url parameter for error messages
    URLorigin = A:ORIGIN('ArchiveURL') -- name of archive url parameter for error messages
    Format = ArchiveFormat or ''; -- swap in archive's format
    end
    end
    elseif is_set (ChapterURL) then -- URL not set so if chapter-url is set apply archive url to it
    elseif is_set (ChapterURL) then -- URL not set so if chapter-url is set apply archive url to it
    OriginalURL = ChapterURL; -- save copy of source chapter's url for archive text
    OriginalURL = ChapterURL; -- save copy of source chapter's url for archive text
    OriginalFormat = ChapterFormat; -- and original |format=
    if 'no' ~= DeadURL then
    if 'no' ~= DeadURL then
    ChapterURL = ArchiveURL -- swap-in the archive's url
    ChapterURL = ArchiveURL -- swap-in the archive's url
    URLorigin = A:ORIGIN('ArchiveURL') -- name of archive url parameter for error messages
    URLorigin = A:ORIGIN('ArchiveURL') -- name of archive url parameter for error messages
    ChapterFormat = ArchiveFormat or ''; -- swap in archive's format
    end
    end
    end
    end
    end
    end


    if in_array(config.CitationClass, {"web","news","journal","pressrelease","podcast", "newsgroup"}) or
    if in_array(config.CitationClass, {"web","news","journal","pressrelease","podcast", "newsgroup", 'arxiv'}) or
    ('citation' == config.CitationClass and is_set (Periodical) and not is_set (Encyclopedia)) then
    ('citation' == config.CitationClass and is_set (Periodical) and not is_set (Encyclopedia)) then
    if is_set (Chapter) or is_set (TransChapter) or is_set (ChapterURL)then -- chapter parameters not supported for these citation types
    if is_set (Chapter) or is_set (TransChapter) or is_set (ChapterURL)then -- chapter parameters not supported for these citation types
    Line 2,270: Line 2,538:
    Chapter = format_chapter_title (Chapter, TransChapter, ChapterURL, ChapterURLorigin);
    Chapter = format_chapter_title (Chapter, TransChapter, ChapterURL, ChapterURLorigin);
    if is_set (Chapter) then
    if is_set (Chapter) then
    ChapterFormat = is_set(ChapterFormat) and " (" .. ChapterFormat .. ")" or "";
    if is_set(ChapterFormat) and not is_set (ChapterURL) then -- Test if |chapter-format= is given without giving a |chapter-url=
    ChapterFormat = ChapterFormat .. set_error( 'format_missing_url', {'chapter-format', 'chapter-url'} );
    end
    if 'map' == config.CitationClass and is_set (TitleType) then
    if 'map' == config.CitationClass and is_set (TitleType) then
    Chapter = Chapter .. ' (' .. TitleType .. ')';
    Chapter = Chapter .. ' ' .. TitleType;
    end
    end
    Chapter = Chapter .. ChapterFormat .. sepc .. ' ';
    Chapter = Chapter .. ChapterFormat .. sepc .. ' ';
    Line 2,286: Line 2,550:
    end
    end


    if in_array(config.CitationClass, {"web","news","journal","pressrelease","podcast", "newsgroup", "mailinglist"}) or
    if in_array(config.CitationClass, {"web","news","journal","pressrelease","podcast", "newsgroup", "mailinglist", 'arxiv'}) or
    ('citation' == config.CitationClass and is_set (Periodical) and not is_set (Encyclopedia)) or
    ('citation' == config.CitationClass and is_set (Periodical) and not is_set (Encyclopedia)) or
    ('map' == config.CitationClass and is_set (Periodical)) then -- special case for cite map when the map is in a periodical treat as an article
    ('map' == config.CitationClass and is_set (Periodical)) then -- special case for cite map when the map is in a periodical treat as an article
    Line 2,316: Line 2,580:
    if is_set(Title) then
    if is_set(Title) then
    if not is_set(TitleLink) and is_set(URL) then  
    if not is_set(TitleLink) and is_set(URL) then  
    Title = external_link( URL, Title ) .. TransError .. Format  
    Title = external_link( URL, Title ) .. TransError .. Format;
    URL = "";
    URL = "";
    Format = "";
    Format = "";
    Line 2,327: Line 2,591:
    Place = " " .. wrap_msg ('written', Place, use_lowercase) .. sepc .. " ";
    Place = " " .. wrap_msg ('written', Place, use_lowercase) .. sepc .. " ";
    end
    end
     
    if is_set(Conference) then
    if is_set (Conference) then
    if is_set(ConferenceURL) then
    if is_set (ConferenceURL) then
    Conference = external_link( ConferenceURL, Conference );
    Conference = external_link( ConferenceURL, Conference );
    end
    end
    Conference = sepc .. " " .. Conference
    Conference = sepc .. " " .. Conference .. ConferenceFormat;
    elseif is_set(ConferenceURL) then
    elseif is_set(ConferenceURL) then
    Conference = sepc .. " " .. external_link( ConferenceURL, nil, ConferenceURLorigin );
    Conference = sepc .. " " .. external_link( ConferenceURL, nil, ConferenceURLorigin );
    end
    end
     
    if not is_set(Position) then
    if not is_set(Position) then
    local Minutes = A['Minutes'];
    local Minutes = A['Minutes'];
    Line 2,378: Line 2,642:
    end
    end
    end
    end
     
    if 'map' == config.CitationClass then -- cite map oddity done after COinS call (and with other in-source locators)
    if is_set (Sheet) or is_set (Sheets) then
    local err_msg1 = 'sheet=, &#124;sheets'; -- default error message in case any of page pages or at are set
    local err_msg2;
    if is_set (Page) or is_set (Pages) or is_set (At) then -- are any set?
    err_msg2 = 'page=, &#124;pages=, &#124;at'; -- a generic error message
    Page = ''; Pages = ''; At = '';
    elseif is_set (Sheet) and is_set (Sheets) then -- if both are set make error message
    err_msg1 = 'sheet';
    err_msg2 = 'sheets';
    end
    if is_set (err_msg2) then
    table.insert( z.message_tail, { set_error( 'redundant_parameters', {wrap_style ('parameter', err_msg1) .. ' and ' .. wrap_style ('parameter', err_msg2)}, true ) } ); -- add error message
    end
    if not is_set (Sheet) then -- do sheet static text and formatting; Sheet has priority over Sheets if both provided
    if is_set (Sheets) then
    if is_set (Periodical) then
    Sheet = ": Sheets " .. Sheets; -- because Sheet has priority, no need to support both later on
    else
    Sheet = sepc .. " Sheets " .. Sheets;
    end
    end
    else
    if is_set (Periodical) then
    Sheet = ": Sheet " .. Sheet;
    else
    Sheet = sepc .. " Sheet " .. Sheet;
    end
    end
    end
    end
     
    At = is_set(At) and (sepc .. " " .. At) or "";
    At = is_set(At) and (sepc .. " " .. At) or "";
    Position = is_set(Position) and (sepc .. " " .. Position) or "";
    Position = is_set(Position) and (sepc .. " " .. Position) or "";
    Line 2,405: Line 2,701:


    Others = is_set(Others) and (sepc .. " " .. Others) or "";
    Others = is_set(Others) and (sepc .. " " .. Others) or "";
    -- handle type parameter for those CS1 citations that have default values
    if in_array(config.CitationClass, {"AV-media-notes", "DVD-notes", "mailinglist", "map", "podcast", "pressrelease", "report", "techreport", "thesis"}) then
    TitleType = set_titletype (config.CitationClass, TitleType);
    if is_set(Degree) and "Thesis" == TitleType then -- special case for cite thesis
    TitleType = Degree .. " thesis";
    end
    end
    if is_set(TitleType) then -- if type parameter is specified
    TitleType = " (" .. TitleType .. ")"; -- display it in parentheses
    end


    TitleNote = is_set(TitleNote) and (sepc .. " " .. TitleNote) or "";
    TitleNote = is_set(TitleNote) and (sepc .. " " .. TitleNote) or "";
    Line 2,469: Line 2,752:
    end
    end


    ID_list = build_id_list( ID_list, {DoiBroken = DoiBroken, ASINTLD = ASINTLD, IgnoreISBN = IgnoreISBN, Embargo=Embargo} );
    ID_list = build_id_list( ID_list, {DoiBroken = DoiBroken, ASINTLD = ASINTLD, IgnoreISBN = IgnoreISBN, Embargo=Embargo, Class = Class} );


    if is_set(URL) then
    if is_set(URL) then
    Line 2,492: Line 2,775:
    if sepc ~= "." then arch_text = arch_text:lower() end
    if sepc ~= "." then arch_text = arch_text:lower() end
    Archived = sepc .. " " .. substitute( cfg.messages['archived-not-dead'],
    Archived = sepc .. " " .. substitute( cfg.messages['archived-not-dead'],
    { external_link( ArchiveURL, arch_text ), ArchiveDate } );
    { external_link( ArchiveURL, arch_text ) .. ArchiveFormat, ArchiveDate } );
    if not is_set(OriginalURL) then
    if not is_set(OriginalURL) then
    Archived = Archived .. " " .. set_error('archive_missing_url');    
    Archived = Archived .. " " .. set_error('archive_missing_url');    
    Line 2,500: Line 2,783:
    if sepc ~= "." then arch_text = arch_text:lower() end
    if sepc ~= "." then arch_text = arch_text:lower() end
    Archived = sepc .. " " .. substitute( arch_text,
    Archived = sepc .. " " .. substitute( arch_text,
    { external_link( OriginalURL, cfg.messages['original'] ), ArchiveDate } );
    { external_link( OriginalURL, cfg.messages['original'] ) .. OriginalFormat, ArchiveDate } ); -- format already styled
    else
    else
    local arch_text = cfg.messages['archived-missing'];
    local arch_text = cfg.messages['archived-missing'];
    Line 2,507: Line 2,790:
    { set_error('archive_missing_url'), ArchiveDate } );
    { set_error('archive_missing_url'), ArchiveDate } );
    end
    end
    elseif is_set (ArchiveFormat) then
    Archived = ArchiveFormat; -- if set and ArchiveURL not set ArchiveFormat has error message
    else
    else
    Archived = ""
    Archived = ""
    end
    end
    local Lay
    local Lay = '';
    if is_set(LayURL) then
    if is_set(LayURL) then
    if is_set(LayDate) then LayDate = " (" .. LayDate .. ")" end
    if is_set(LayDate) then LayDate = " (" .. LayDate .. ")" end
    Line 2,520: Line 2,805:
    end
    end
    if sepc == '.' then
    if sepc == '.' then
    Lay = sepc .. " " .. external_link( LayURL, cfg.messages['lay summary'] ) .. LaySource .. LayDate
    Lay = sepc .. " " .. external_link( LayURL, cfg.messages['lay summary'] ) .. LayFormat .. LaySource .. LayDate
    else
    else
    Lay = sepc .. " " .. external_link( LayURL, cfg.messages['lay summary']:lower() ) .. LaySource .. LayDate
    Lay = sepc .. " " .. external_link( LayURL, cfg.messages['lay summary']:lower() ) .. LayFormat .. LaySource .. LayDate
    end
    end
    else
    elseif is_set (LayFormat) then -- Test if |lay-format= is given without giving a |lay-url=
    Lay = "";
    Lay = sepc .. LayFormat; -- if set and LayURL not set, then LayFormat has error message
    end
    end
     
    if is_set(Transcript) then
    if is_set(Transcript) then
    if is_set(TranscriptURL) then Transcript = external_link( TranscriptURL, Transcript ); end
    if is_set(TranscriptURL) then
    Transcript = external_link( TranscriptURL, Transcript );
    end
    Transcript = sepc .. ' ' .. Transcript .. TranscriptFormat;
    elseif is_set(TranscriptURL) then
    elseif is_set(TranscriptURL) then
    Transcript = external_link( TranscriptURL, nil, TranscriptURLorigin );
    Transcript = external_link( TranscriptURL, nil, TranscriptURLorigin );
    end
    end
     
    local Publisher;
    local Publisher;
    if is_set(Periodical) and
    if is_set(Periodical) and
    Line 2,616: Line 2,904:
    end
    end
    elseif 'episode' == config.CitationClass then -- special case for cite episode
    tcommon = safe_join( {Title, TitleNote, TitleType, Series, Transcript, Language, Edition, Publisher}, sepc );
    else -- all other CS1 templates
    else -- all other CS1 templates
    tcommon = safe_join( {Title, TitleNote, Conference, Periodical, Format, TitleType, Series, Language,  
    tcommon = safe_join( {Title, TitleNote, Conference, Periodical, Format, TitleType, Series, Language,  
    Line 2,629: Line 2,919:
    local idcommon = safe_join( { ID_list, URL, Archived, AccessDate, Via, SubscriptionRequired, Lay, Quote }, sepc );
    local idcommon = safe_join( { ID_list, URL, Archived, AccessDate, Via, SubscriptionRequired, Lay, Quote }, sepc );
    local text;
    local text;
    local pgtext = Position .. Page .. Pages .. At;
    local pgtext = Position .. Sheet .. Page .. Pages .. At;
    if is_set(Authors) then
    if is_set(Authors) then
    Line 2,660: Line 2,950:
    if (sepc ~= '.') then in_text = in_text:lower() end
    if (sepc ~= '.') then in_text = in_text:lower() end
    Editors = in_text .. Editors .. post_text;
    Editors = in_text .. Editors .. post_text;
    if (string.sub(Editors,-1,-1) == sepc)
    if (string.sub(Editors,-1,-1) == sepc) or (string.sub(Editors,-3,-1) == sepc .. ']]') then -- if last editor name ends with sepc char
    then Editors = Editors .. " "
    Editors = Editors .. " "; -- don't add another
    else Editors = Editors .. sepc .. " "
    else
    Editors = Editors .. sepc .. " " -- otherwise terninate the editor list
    end
    end
    end
    end