Module:Citation/CS1: Difference between revisions

    From Nonbinary Wiki
    m>Trappist the monk
    (ISSN error detection; Expand uncategorized namespaces; Reg/Sub required msg tweak; Trap coauthors without authors; Hide error messages;)
    m>Trappist the monk
    (Synch from sandbox; Migrate cite thesis and cite techreport; ISBN & ISSN tweaks; Deprecated parameter tracking; Add date validation;)
    Line 6: Line 6:


    -- Include translation message hooks, ID and error handling configuration settings.
    -- Include translation message hooks, ID and error handling configuration settings.
    local cfg = mw.loadData( 'Module:Citation/CS1/Configuration' );
    local cfg = mw.loadData( 'Module:Citation/CS1/Configuration/sandbox' );


    -- Contains a list of all recognized parameters
    -- Contains a list of all recognized parameters
    local whitelist = mw.loadData( 'Module:Citation/CS1/Whitelist' );
    local whitelist = mw.loadData( 'Module:Citation/CS1/Whitelist/sandbox' );


    -- Whether variable is set or not
    -- Whether variable is set or not
    Line 37: Line 37:
         end
         end
         return false;
         return false;
    end
    -- Add this page to the deprecated parameter tracking category
    function deprecated_parameter()
    if true ~= Page_in_deprecated_cat then -- if we haven't been here before then set a
    Page_in_deprecated_cat=true; -- sticky flag so that if there are more than one deprecated parameter the category is added only once
    table.insert( z.error_categories, "Pages containing cite templates with deprecated parameters" ); -- add page to category
    end
    end
    end


    Line 307: Line 315:
    ]]
    ]]
    function issn(id)
    function issn(id)
    local clean_issn;
    local issn_copy = id; -- save a copy of unadulterated issn; use this version for display if issn does not validate
    local issn_copy = id; -- save a copy of unadulterated issn; use this version for display if issn does not validate
    local handler = cfg.id_handlers['ISSN'];
    local handler = cfg.id_handlers['ISSN'];
    local temp = 0;
    local text;
    local text;
    local valid_issn = true;
    local valid_issn = true;


    id=id:gsub( "[%s-–]", "" ); -- strip spaces, hyphens, and ndashes from the issn
    id=id:gsub( "[%s-–]", "" ); -- strip spaces, hyphens, and ndashes from the issn
    clean_issn=string.sub( id, 1, 4 ) .. "-" .. string.sub( id, 5 ); -- make a copy with a hyphen after 4 digits; use this version for display if issn validates


    if 8 ~= id:len() or nil == id:match( "^%d*X?$" ) then -- validate the issn: 8 didgits long, containing only 0-9 or X in the last position
    if 8 ~= id:len() or nil == id:match( "^%d*X?$" ) then -- validate the issn: 8 didgits long, containing only 0-9 or X in the last position
    valid_issn=false; -- wrong length or improper character
    valid_issn=false; -- wrong length or improper character
    else
    else
    id = { id:byte(1, 8) }; -- table of individual bytes
    valid_issn=is_valid_isxn(id, 8); -- validate issn
    for i, v in ipairs( id ) do -- loop through all of the byte an calculate the checksum
    if v == string.byte( "X" ) then -- if checkdigit is X
    temp = temp + 10*( 9 - i ); -- it represents 10 decimal
    else
    temp = temp + tonumber( string.char(v) )*(9-i);
    end
    end
    valid_issn = temp % 11 == 0; -- checksum must be zero for valid issn
    end
    end


    if true == valid_issn then
    if true == valid_issn then
    id = clean_issn; -- if valid, use the cleaned-up version for the display
    id = string.sub( id, 1, 4 ) .. "-" .. string.sub( id, 5 ); -- if valid, display correctly formatted version
    else
    else
    id = issn_copy; -- if not valid, use the show the invalid issn with error message
    id = issn_copy; -- if not valid, use the show the invalid issn with error message
    Line 347: Line 344:
    end
    end


    -- returns a number according to the month in a date 1 for January, etc.  If not a valid month, returns 0
    function get_month_number (month)
    local long_months = {['january']=1, ['february']=2, ['march']=3, ['april']=4, ['may']=5, ['june']=6, ['july']=7, ['august']=8, ['september']=9, ['october']=10, ['november']=11, ['december']=12};
    local short_months = {['jan']=1, ['feb']=2, ['mar']=3, ['apr']=4, ['may']=5, ['jun']=6, ['jul']=7, ['aug']=8, ['sep']=9, ['oct']=10, ['nov']=11, ['dec']=12};
    local temp;
    temp=long_months[month:lower()];
    if temp then return temp; end -- if month is the long-form name
    temp=short_months[month:lower()];
    if temp then return temp; end -- if month is the short-form name
    return 0; -- misspelled or not a month name
    end
    -- returns true if date has one of the five seasons.  Else false.
    function is_valid_season (season)
    if inArray( season, {'winter', 'spring', 'summer', 'fall', 'autumn'} ) then
    return true;
    end
    return false;
    end


    --[[
    --[[
    Determines whether an URL string is valid
    Returns true if day is less than or equal to the number of days in month; else returns false.


    At present the only check is whether the string appears to  
    Assumes Julian calendar prior to year 1582 and Gregorian calendar thereafter. Accounts for Julian calendar leap years before 1582 and Gregorian leap years after 1582.
    be prefixed with a URI scheme. It is not determined whether
    Where the two calendars overlap (1582 to approximately 1923) dates are assumed to be Gregorian.
    the URI scheme is valid or whether the URL is otherwise well
    formed.
    ]]
    ]]
    function checkurl( url_str )
    function is_valid_date (year, month, day)
        -- Protocol-relative or URL scheme
    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        return url_str:sub(1,2) == "//" or url_str:match( "^[^/]*:" ) ~= nil;
    local month_length;
    if (2==month) then -- if February
    month_length = 28; -- then 28 days unless
    if 1582 > tonumber(year) then -- Julian calendar
    if 0==(year%4) then
    month_length = 29;
    end
    else -- Gregorian calendar
    if (0==(year%4) and (0~=(year%100) or 0==(year%400))) then -- date specifies a leap year
    month_length = 29; -- if leap year then 29 days in February
    end
    end
    else
    month_length=days_in_month[month];
    end
     
    if tonumber (day) > month_length then
    return false;
    end
    return true;
    end
    end


    -- Removes irrelevant text and dashes from ISBN number
    --Check a pair of months or seasons to see if both are valid members of a month or season pair.
    -- Similar to that used for Special:BookSources
     
    function cleanisbn( isbn_str )
    function is_valid_month_season_range(range_start, range_end)
        return isbn_str:gsub( "[^-0-9X]", "" );
    if 0 == get_month_number (range_start:lower()) then -- is this a month range?
    if true == is_valid_season (range_start:lower()) then -- not a month range, is this a season range?
    return is_valid_season (range_end:lower()); -- range_start is season; return true if range_end also a season; else false
    end
    return false; -- range_start is not a month or a season
    end
    if 0 == get_month_number (range_end:lower()) then -- range_start is a month; is range_end also a  month?
    return false; -- not a month range
    end
    return true;
    end
    end


    -- Determines whether an ISBN string is valid
    function checkisbn( isbn_str )
        isbn_str = cleanisbn( isbn_str ):gsub( "-", "" );
        local len = isbn_str:len();
        if len ~= 10 and len ~= 13 then
            return false;
        end
        local temp = 0;
        if len == 10 then
            if isbn_str:match( "^%d*X?$" ) == nil then return false; end
            isbn_str = { isbn_str:byte(1, len) };
            for i, v in ipairs( isbn_str ) do
                if v == string.byte( "X" ) then
                    temp = temp + 10*( 11 - i );
                else
                    temp = temp + tonumber( string.char(v) )*(11-i);
                end
            end
            return temp % 11 == 0;
        else
            if isbn_str:match( "^%d*$" ) == nil then return false; end
            isbn_str = { isbn_str:byte(1, len) };
            for i, v in ipairs( isbn_str ) do
                temp = temp + (3 - 2*(i % 2)) * tonumber( string.char(v) );
            end
            return temp % 10 == 0;
        end
    end


    -- Gets the display text for a wikilink like [[A|B]] or [[B]] gives B
    --[[
    function removewikilink( str )
    Check date format to see that it is one of the formats approved by MOS:DATE: MMMM D, YYYY; D MMMM YYYY; MMMM YYYY; YYYY-MM-DD; YYYY.
        return (str:gsub( "%[%[([^%[%]]*)%]%]", function(l)
    Additionally, check the date to see that it is a real date: no 31 in 30-day months; no 29 February when not a leap year.  Months, both long-form and three
            return l:gsub( "^[^|]*|(.*)$", "%1" ):gsub("^%s*(.-)%s*$", "%1");
    character abbreviations, and seasons must be spelled correctly.
        end));
     
    end
    If the date fails the fomat tests, this function returns false but does not return values for anchor_year and COinS_date.  When this happens, the date parameter is
    used in the COinS metadata and the CITEREF identifier gets its year from the year parameter if present.
     
    Inputs:
    date_string - date string from date-holding parameters (date, year, accessdate, embargo, archivedate, etc)
     
    Returns:
    false if date string is not a real date; else
    true, anchor_year, COinS_date
    anchor_year can be used in CITEREF anchors
    COinS_date is date_string without anchor_year disambiguators if any
    ]]
    function check_date (date_string)
    local year;
    local month;
    local day;
    local anchor_year;
    local coins_date;
     
    if date_string:match("^%d%d%d%d%-%d%d%-%d%d$") then -- Year-initial numerical year month day format
    coins_date = date_string:match("%d%d%d%d%-%d%d%-%d%d");
    year, month, day=string.match(date_string, "(%d%d%d%d)%-(%d%d)%-(%d%d)");
    anchor_year = year;
    month=tonumber(month);
    if 12 < month or 1 > month then return false; end
     
    elseif date_string:match("^%a+%s*%d%d*%s*,%s*%d%d%d%d%a?$") then -- month-initial: month day, year
    coins_date = date_string:match("%a+%s*%d%d*%s*,%s*%d%d%d%d");
    month, day, anchor_year, year=string.match(date_string, "(%a+)%s*(%d%d*)%s*,%s*((%d%d%d%d)%a?)");
    month = get_month_number (month:lower());
    if 0 == month then return false; end -- return false if month text isn't one of the twelve months
    elseif date_string:match("^%d%d*%s*%a+%s*%d%d%d%d%a?$") then -- date-initial: day month year
    coins_date = date_string:match("%d%d*%s*%a+%s*%d%d%d%d");
    day, month, anchor_year, year=string.match(date_string, "(%d%d*)%s*(%a+)%s*((%d%d%d%d)%a?)");
    month = get_month_number (month:lower());
    if 0 == month then return false; end -- return false if month text isn't one of the twelve months
     
    elseif mw.ustring.match (date_string, "^%a+%s*[%s%-/–]%s*%a+%s*%d%d%d%d%a?$") then -- month/season range year
    local month2
    coins_date = mw.ustring.match (date_string, "%a+%s*[%s%-/–]%s*%a+%s*%d%d%d%d");
    coins_date= mw.ustring.gsub( coins_date, "–", "-" ); -- replace ndash with hyphen
    month, month2, anchor_year, year=mw.ustring.match (date_string, "(%a+)%s*[%s%-/–]%s*(%a+)%s*((%d%d%d%d)%a?)");
    day=0; -- mark day as not used
    if false == is_valid_month_season_range(month, month2) then
    return false;
    end
    elseif date_string:match("^%a+%s*%d%d%d%d%a?$") then -- month/season year
    coins_date = date_string:match("%a+%s*%d%d%d%d");
    month, anchor_year, year=string.match(date_string, "(%a+)%s*((%d%d%d%d)%a?)");
    day=0; -- mark day as not used
    local season=month; -- copy
    month = get_month_number (month:lower());
    if month == 0 then -- if month text isn't one of the twelve months, might be a season
    if false == is_valid_season (season:lower()) then
    return false; -- return false not a month or one of the five seasons
    end
    end
     
    elseif date_string:match("^%d%d%d%d?%a?$") then -- year; here accept either YYY or YYYY
    coins_date = date_string:match("^%d%d%d%d?");
    anchor_year, year=string.match(date_string, "((%d%d%d%d?)%a?)");
    month, day = 0, 0; -- mark day and month as not used
    else
    return false; -- date format not one of the MOS:DATE approved formats
    end


    -- Escape sequences for content that will be used for URL descriptions
    if 0~=month and 0~=day then -- check year month day dates for validity
    function safeforurl( str )
    if false==is_valid_date(year,month,day) then
        if str:match( "%[%[.-%]%]" ) ~= nil then  
    return false; -- date string is not a real date return false; unset anchor_year and coins_date
            table.insert( z.message_tail, { seterror( 'wikilink_in_url', {}, true ) } );
    end
        end
    end
       
        return str:gsub( '[%[%]\n]', {   
    return true, anchor_year, coins_date; -- format is good and date string represents a real date
            ['['] = '&#91;',
    end
            [']'] = '&#93;',
     
            ['\n'] = ' ' } );
    --[[
    end
    Cycle the date-holding parameters in passed table date_parameters_list through check_date() to check compliance with MOS:DATE. For all valid dates, check_date() returns
    true and values for anchor_year (used in CITEREF identifiers) and COinS_date (used in the COinS metadata).  The |date= parameter test is unique.  This function only
    accepts anchor_year and COinS_date results from the |date= parameter test and |date= is the only date-holding parameter that is allowed to contain the no-date keywords
    "n.d." or "nd" (without quotes).
     
    Unlike most error messages created in this module, only one error message is created by this function. Because all of the date holding parameters are processed serially,
    a single error message is created as the dates are tested.
    ]]
     
    function dates(date_parameters_list)
    local anchor_year; -- will return as nil if the date being tested is not |date=
    local COinS_date; -- will return as nil if the date being tested is not |date=
    local error_message ="";
    local good_date=false;
    for k, v in pairs(date_parameters_list) do -- for each date-holding parameter in the list
    if is_set(v) then -- if the parameter has a value
    if v:match("^c%.%s%d%d%d%d?%a?$") then -- special case for c. year or with or without CITEREF disambiguator - only |date= and |year=
    if 'date'==k then
    good_date, anchor_year, COinS_date = true, v:match("((c%.%s%d%d%d%d?)%a?)"); -- anchor year and COinS_date only from |date= parameter
    elseif 'year'==k then
    good_date =  true;
    end
    elseif 'year'==k then -- if the parameter is |year= (but not c. year)
    if v:match("^%d%d%d%d?%a?$") then -- year with or without CITEREF disambiguator
    good_date =  true;
    end
    elseif 'date'==k then -- if the parameter is |date=
    if v:match("n%.d%.%a?") then -- if |date=n.d. with or without a CITEREF disambiguator
    good_date, anchor_year, COinS_date = true, v:match("((n%.d%.)%a?)"); --"n.d."; -- no error when date parameter is set to no date
    elseif v:match("nd%a?$") then -- if |date=nd with or without a CITEREF disambiguator
    good_date, anchor_year, COinS_date = true, v:match("((nd)%a?)"); --"nd"; -- no error when date parameter is set to no date
    else
    good_date, anchor_year, COinS_date = check_date (v); -- go test the date
    end
    else -- any other date-holding parameter
    good_date = check_date (v); -- go test the date
    end
    if false==good_date then -- assemble one error message so we don't add the tracking category multiple times
    if is_set(error_message) then -- once we've added the first portion of the error message ...
    error_message=error_message .. ", "; -- ... add a comma space separator
    end
    error_message=error_message .. "&#124;" .. k .. "="; -- add the failed parameter
    end
    end
    end
    if is_set(error_message) then
    table.insert( z.message_tail, { seterror( 'bad_date', {error_message}, true ) } ); -- add this error message
    end


    -- Converts a hyphen to a dash
    return anchor_year, COinS_date; -- and done
    function hyphentodash( str )
        if not is_set(str) or str:match( "[%[%]{}<>]" ) ~= nil then
            return str;
        end   
        return str:gsub( '-', '–' );
    end
    end


    -- Protects a string that will be wrapped in wiki italic markup '' ... ''
    --[[
    function safeforitalics( str )
    Determines whether an URL string is valid
        --[[ Note: We can not use <i> for italics, as the expected behavior for
     
        italics specified by ''...'' in the title is that they will be inverted
    At present the only check is whether the string appears to
        (i.e. unitalicized) in the resulting references.  In addition, <i> and ''
    be prefixed with a URI scheme. It is not determined whether
        tend to interact poorly under Mediawiki's HTML tidy. ]]
    the URI scheme is valid or whether the URL is otherwise well
       
    formed.
        if not is_set(str) then
    ]]
            return str;
    function checkurl( url_str )
         else
        -- Protocol-relative or URL scheme
            if str:sub(1,1) == "'" then str = "<span />" .. str; end
         return url_str:sub(1,2) == "//" or url_str:match( "^[^/]*:" ) ~= nil;
            if str:sub(-1,-1) == "'" then str = str .. "<span />"; end
           
            -- Remove newlines as they break italics.
            return str:gsub( '\n', ' ' );
        end
    end
    end


    --[[
    -- Removes irrelevant text and dashes from ISBN number
    Joins a sequence of strings together while checking for duplicate separation
    -- Similar to that used for Special:BookSources
    characters.
    function cleanisbn( isbn_str )
    ]]
        return isbn_str:gsub( "[^-0-9X]", "" );
    function safejoin( tbl, duplicate_char )
    end
        --[[
     
        Note: we use string functions here, rather than ustring functions.
    --[[
       
    ISBN-10 and ISSN validator code calculates checksum across all isbn/issn digits including the check digit. If the number is valid the result will be 0.
        This has considerably faster performance and should work correctly as
    Before calling this function, issbn/issn must be checked for length and stripped of dashes, spaces and other non-isxn characters.
        long as the duplicate_char is strict ASCII. The strings
    ]]
        in tbl may be ASCII or UTF8.
    function is_valid_isxn (isxn_str, len)
        ]]
    local temp = 0;
       
    isxn_str = { isxn_str:byte(1, len) }; -- make a table of bytes
        local str = '';
    len = len+1; -- adjust to be a loop counter
        local comp = '';
    for i, v in ipairs( isxn_str ) do -- loop through all of the byte an calculate the checksum
        local end_chr = '';
    if v == string.byte( "X" ) then -- if checkdigit is X
        local trim;
    temp = temp + 10*( len - i ); -- it represents 10 decimal
        for _, value in ipairs( tbl ) do
    else
            if value == nil then value = ''; end
    temp = temp + tonumber( string.char(v) )*(len-i);
           
    end
            if str == '' then
    end
                str = value;
    return temp % 11 == 0; -- returns true if calculation result is zero
            elseif value ~= '' then
    end
                if value:sub(1,1) == '<' then
     
                    -- Special case of values enclosed in spans and other markup.
    -- Determines whether an ISBN string is valid
                    comp = value:gsub( "%b<>", "" );
    function checkisbn( isbn_str )
                else
        isbn_str = cleanisbn( isbn_str ):gsub( "-", "" );
                    comp = value;
        local len = isbn_str:len();
                end
               
        if len ~= 10 and len ~= 13 then
                if comp:sub(1,1) == duplicate_char then
            return false;
                    trim = false;
        end
                    end_chr = str:sub(-1,-1);
                    -- str = str .. "<HERE(enchr=" .. end_chr.. ")"
        if len == 10 then
                    if end_chr == duplicate_char then
            if isbn_str:match( "^%d*X?$" ) == nil then return false; end
                        str = str:sub(1,-2);
    return is_valid_isxn(isbn_str, 10);
                    elseif end_chr == "'" then
        else
                        if str:sub(-3,-1) == duplicate_char .. "''" then
        local temp = 0;
                            str = str:sub(1, -4) .. "''";
            if isbn_str:match( "^97[89]%d*$" ) == nil then return false; end -- isbn13 begins with 978 or 979
                        elseif str:sub(-5,-1) == duplicate_char .. "]]''" then
            isbn_str = { isbn_str:byte(1, len) };
                            trim = true;
            for i, v in ipairs( isbn_str ) do
                        elseif str:sub(-4,-1) == duplicate_char .. "]''" then
                temp = temp + (3 - 2*(i % 2)) * tonumber( string.char(v) );
                            trim = true;
            end
                        end
            return temp % 10 == 0;
                    elseif end_chr == "]" then
        end
                        if str:sub(-3,-1) == duplicate_char .. "]]" then
    end
                            trim = true;
                        elseif str:sub(-2,-1) == duplicate_char .. "]" then
                            trim = true;
                        end
                    elseif end_chr == " " then
                        if str:sub(-2,-1) == duplicate_char .. " " then
                            str = str:sub(1,-3);
                        end
                    end


                    if trim then
    -- Gets the display text for a wikilink like [[A|B]] or [[B]] gives B
                        if value ~= comp then
    function removewikilink( str )
                            local dup2 = duplicate_char;
        return (str:gsub( "%[%[([^%[%]]*)%]%]", function(l)
                            if dup2:match( "%A" ) then dup2 = "%" .. dup2; end
            return l:gsub( "^[^|]*|(.*)$", "%1" ):gsub("^%s*(.-)%s*$", "%1");
                           
        end));
                            value = value:gsub( "(%b<>)" .. dup2, "%1", 1 )
    end
                        else
     
                            value = value:sub( 2, -1 );
    -- Escape sequences for content that will be used for URL descriptions
                        end
    function safeforurl( str )
                    end
        if str:match( "%[%[.-%]%]" ) ~= nil then
                end
            table.insert( z.message_tail, { seterror( 'wikilink_in_url', {}, true ) } );
                str = str .. value;
            end
         end
         end
         return str;
       
    end 
         return str:gsub( '[%[%]\n]', {   
     
            ['['] = '&#91;',
    --[[
            [']'] = '&#93;',
    Return the year portion of a date string, if possible. 
            ['\n'] = ' ' } );
    Returns empty string if the argument can not be interpreted
    end
    as a year.
     
    ]]
    -- Converts a hyphen to a dash
    function selectyear( str )
    function hyphentodash( str )
        -- Is the input a simple number?
         if not is_set(str) or str:match( "[%[%]{}<>]" ) ~= nil then
        local num = tonumber( str );
         if num ~= nil and num > 0 and num < 2100 and num == math.floor(num) then
             return str;
             return str;
         else
         end   
            -- Use formatDate to interpret more complicated formats
        return str:gsub( '-', '' );
            local lang = mw.getContentLanguage();
            local good, result;
            good, result = pcall( lang.formatDate, lang, 'Y', str );
            if good then
                return result;
            else
                -- extract year if the date uses seasons
                str=string.lower (str);
                local seasons={"winter", "spring", "summer", "fall", "autumn"};
                local date_string_split=mw.text.split (str, "[%s%-/–]");      -- split date string into parts; white space, hyphen, forward slash, and ndash are allowed separators
                local has_season=false;
               
                for n,season_value in ipairs(seasons) do                -- for each season ...
                    for n,split_value in ipairs(date_string_split) do  -- ... loop through date string values
                        if split_value == season_value then            -- does the split value match the season value?
                            if has_season==false then                  -- found one. if this one is the first we've found ...
                                has_season=true;                        -- ... remember that we found a season
                            end
                        elseif has_season==true then                    -- if split_value isn't a season, and we've previously found a season ...
                            num = tonumber( split_value );              -- ... convert current split value to a number if we can
                            if num ~= nil and num > 0 and num < 2100 and num == math.floor(num) then    -- if it's a suitable number
                                return tostring( num );                                                 -- return it as a string
                            end -- if num
                        end -- if string.find
                    end -- for split value loop
                end -- season value loop
            end -- if good
        end -- if num
    end -- selectyear
     
    -- Attempts to convert names to initials.
    function reducetoinitials(first)
        local initials = {}
        for word in string.gmatch(first, "%S+") do
            table.insert(initials, string.sub(word,1,1)) -- Vancouver format does not include full stops.
        end
        return table.concat(initials) -- Vancouver format does not include spaces.
    end
    end


    -- Formats a list of people (e.g. authors / editors)
    -- Protects a string that will be wrapped in wiki italic markup '' ... ''
    function listpeople(control, people)
    function safeforitalics( str )
         local sep = control.sep;
         --[[ Note: We can not use <i> for italics, as the expected behavior for
         local namesep = control.namesep
         italics specified by ''...'' in the title is that they will be inverted
         local format = control.format
         (i.e. unitalicized) in the resulting references. In addition, <i> and ''
        local maximum = control.maximum
         tend to interact poorly under Mediawiki's HTML tidy. ]]
         local lastauthoramp = control.lastauthoramp;
        local text = {}
        local etal = false;
          
          
         if sep:sub(-1,-1) ~= " " then sep = sep .. " " end
         if not is_set(str) then
        if maximum ~= nil and maximum < 1 then return "", 0; end
            return str;
          
         else
        for i,person in ipairs(people) do
             if str:sub(1,1) == "'" then str = "<span />" .. str; end
             if is_set(person.last) then
            if str:sub(-1,-1) == "'" then str = str .. "<span />"; end
                local mask = person.mask
           
                local one
            -- Remove newlines as they break italics.
                local sep_one = sep;
            return str:gsub( '\n', ' ' );
                if maximum ~= nil and i > maximum then
                    etal = true;
                    break;
                elseif (mask ~= nil) then
                    local n = tonumber(mask)
                    if (n ~= nil) then
                        one = string.rep("&mdash;",n)
                    else
                        one = mask;
                        sep_one = " ";
                    end
                else
                    one = person.last
                    local first = person.first
                    if is_set(first) then
                        if ( "vanc" == format ) then first = reducetoinitials(first) end
                        one = one .. namesep .. first
                    end
                    if is_set(person.link) then one = "[[" .. person.link .. "|" .. one .. "]]" end
                end
                table.insert( text, one )
                table.insert( text, sep_one )
            end
         end
         end
    end


        local count = #text / 2;
    --[[
        if count > 0 then
    Joins a sequence of strings together while checking for duplicate separation
            if count > 1 and is_set(lastauthoramp) and not etal then
    characters.
                text[#text-2] = " & ";
    ]]
            end
    function safejoin( tbl, duplicate_char )
            text[#text] = nil;
        --[[
         end
         Note: we use string functions here, rather than ustring functions.
          
          
         local result = table.concat(text) -- construct list
         This has considerably faster performance and should work correctly as
         if etal then
         long as the duplicate_char is strict ASCII. The strings
            local etal_text = cfg.messages['et al'];
        in tbl may be ASCII or UTF8.
            result = result .. " " .. etal_text;
         ]]
         end
          
          
         -- if necessary wrap result in <span> tag to format in Small Caps
         local str = '';
        if ( "scap" == format ) then result =  
        local comp = '';
             '<span class="smallcaps" style="font-variant:small-caps">' .. result .. '</span>';
        local end_chr = '';
        end
        local trim;
        return result, count
        for _, value in ipairs( tbl ) do
    end
            if value == nil then value = ''; end
     
              
    -- Generates a CITEREF anchor ID.
            if str == '' then
    function anchorid( options )
                str = value;
        return "CITEREF" .. table.concat( options );
            elseif value ~= '' then
    end
                if value:sub(1,1) == '<' then
     
                    -- Special case of values enclosed in spans and other markup.
    -- Gets name list from the input arguments
                    comp = value:gsub( "%b<>", "" );
    function extractnames(args, list_name)
                else
        local names = {};
                    comp = value;
        local i = 1;
                end
        local last;
               
       
                if comp:sub(1,1) == duplicate_char then
        while true do
                    trim = false;
            last = selectone( args, cfg.aliases[list_name .. '-Last'], 'redundant_parameters', i );
                    end_chr = str:sub(-1,-1);
            if not is_set(last) then
                    -- str = str .. "<HERE(enchr=" .. end_chr.. ")"
                -- just in case someone passed in an empty parameter
                    if end_chr == duplicate_char then
                break;
                        str = str:sub(1,-2);
            end
                    elseif end_chr == "'" then
            names[i] = {
                        if str:sub(-3,-1) == duplicate_char .. "''" then
                last = last,
                            str = str:sub(1, -4) .. "''";
                first = selectone( args, cfg.aliases[list_name .. '-First'], 'redundant_parameters', i ),
                        elseif str:sub(-5,-1) == duplicate_char .. "]]''" then
                link = selectone( args, cfg.aliases[list_name .. '-Link'], 'redundant_parameters', i ),
                            trim = true;
                mask = selectone( args, cfg.aliases[list_name .. '-Mask'], 'redundant_parameters', i )
                        elseif str:sub(-4,-1) == duplicate_char .. "]''" then
            };
                            trim = true;
            i = i + 1;
                        end
        end
                    elseif end_chr == "]" then
        return names;
                        if str:sub(-3,-1) == duplicate_char .. "]]" then
    end
                            trim = true;
                        elseif str:sub(-2,-1) == duplicate_char .. "]" then
                            trim = true;
                        end
                    elseif end_chr == " " then
                        if str:sub(-2,-1) == duplicate_char .. " " then
                            str = str:sub(1,-3);
                        end
                    end


    -- Populates ID table from arguments using configuration settings
                    if trim then
    function extractids( args )
                        if value ~= comp then
        local id_list = {};
                            local dup2 = duplicate_char;
        for k, v in pairs( cfg.id_handlers ) do   
                            if dup2:match( "%A" ) then dup2 = "%" .. dup2; end
            v = selectone( args, v.parameters, 'redundant_parameters' );
                           
            if is_set(v) then id_list[k] = v; end
                            value = value:gsub( "(%b<>)" .. dup2, "%1", 1 )
                        else
                            value = value:sub( 2, -1 );
                        end
                    end
                end
                str = str .. value;
            end
         end
         end
         return id_list;
         return str;
    end 
     
    --[[
    Return the year portion of a date string, if possible. 
    Returns empty string if the argument can not be interpreted
    as a year.
     
    BUG: If editors set |date=n.d or |date=n.da then selectyear() returns the current year for use in CITEREF.  These "dates" are caught by dates().
    ]]
    function selectyear( str )
    -- Is the input a simple number?
    local num = tonumber( str );
    if num ~= nil and num > 0 and num < 2100 and num == math.floor(num) then
    return str;
    else
    -- Use formatDate to interpret more complicated formats
    local lang = mw.getContentLanguage();
    local good, result;
    good, result = pcall( lang.formatDate, lang, 'Y', str );
    if good then return result; end -- if good
    end
    end
    end


    -- Takes a table of IDs and turns it into a table of formatted ID outputs.
    -- Attempts to convert names to initials.
    function buildidlist( id_list, options )
    function reducetoinitials(first)
         local new_list, handler = {};
        local initials = {}
          
        for word in string.gmatch(first, "%S+") do
         function fallback(k) return { __index = function(t,i) return cfg.id_handlers[k][i] end } end;
            table.insert(initials, string.sub(word,1,1)) -- Vancouver format does not include full stops.
        end
        return table.concat(initials) -- Vancouver format does not include spaces.
    end
     
    -- Formats a list of people (e.g. authors / editors)
    function listpeople(control, people)
         local sep = control.sep;
        local namesep = control.namesep
        local format = control.format
        local maximum = control.maximum
        local lastauthoramp = control.lastauthoramp;
        local text = {}
        local etal = false;
          
         if sep:sub(-1,-1) ~= " " then sep = sep .. " " end
        if maximum ~= nil and maximum < 1 then return "", 0; end
          
          
         for k, v in pairs( id_list ) do
         for i,person in ipairs(people) do
             -- fallback to read-only cfg
             if is_set(person.last) then
            handler = setmetatable( { ['id'] = v }, fallback(k) );
                 local mask = person.mask
           
                local one
            if handler.mode == 'external' then
                 local sep_one = sep;
                 table.insert( new_list, {handler.label, externallinkid( handler ) } );
                if maximum ~= nil and i > maximum then
            elseif handler.mode == 'internal' then
                    etal = true;
                 table.insert( new_list, {handler.label, internallinkid( handler ) } );
                    break;
            elseif handler.mode ~= 'manual' then
                elseif (mask ~= nil) then
                error( cfg.messages['unknown_ID_mode'] );
                    local n = tonumber(mask)
            elseif k == 'DOI' then
                    if (n ~= nil) then
                table.insert( new_list, {handler.label, doi( v, options.DoiBroken ) } );
                        one = string.rep("&mdash;",n)
            elseif k == 'ASIN' then
                    else
                table.insert( new_list, {handler.label, amazon( v, options.ASINTLD ) } );  
                        one = mask;
            elseif k == 'OL' then
                        sep_one = " ";
                 table.insert( new_list, {handler.label, openlibrary( v ) } );
                    end
            elseif k == 'PMC' then
                 else
                table.insert( new_list, {handler.label, pmc( v, options.Embargo ) } );
                    one = person.last
            elseif k == 'ISSN' then
                    local first = person.first
            table.insert( new_list, {handler.label, issn( v ) } );
                    if is_set(first) then
            elseif k == 'ISBN' then
                        if ( "vanc" == format ) then first = reducetoinitials(first) end
                local ISBN = internallinkid( handler );
                        one = one .. namesep .. first
                if not checkisbn( v ) and not is_set(options.IgnoreISBN) then
                    end
                    ISBN = ISBN .. seterror( 'bad_isbn', {}, false, " ", "" );
                    if is_set(person.link) then one = "[[" .. person.link .. "|" .. one .. "]]" end
                 end
                 end
                 table.insert( new_list, {handler.label, ISBN } );              
                 table.insert( text, one )
             else
                table.insert( text, sep_one )
                 error( cfg.messages['unknown_manual_ID'] );
            end
        end
     
        local count = #text / 2;
        if count > 0 then
             if count > 1 and is_set(lastauthoramp) and not etal then
                 text[#text-2] = " & ";
             end
             end
            text[#text] = nil;
         end
         end
          
          
         function comp( a, b )
         local result = table.concat(text) -- construct list
             return a[1] < b[1];
        if etal then
             local etal_text = cfg.messages['et al'];
            result = result .. " " .. etal_text;
         end
         end
          
          
         table.sort( new_list, comp );
         -- if necessary wrap result in <span> tag to format in Small Caps
         for k, v in ipairs( new_list ) do
         if ( "scap" == format ) then result =
             new_list[k] = v[2];
             '<span class="smallcaps" style="font-variant:small-caps">' .. result .. '</span>';
         end
         end  
          
         return result, count
         return new_list;
    end
     
    -- Generates a CITEREF anchor ID.
    function anchorid( options )
         return "CITEREF" .. table.concat( options );
    end
    end
     
     
    -- Chooses one matching parameter from a list of parameters to consider
    -- Gets name list from the input arguments
    -- Generates an error if more than one match is present.
    function extractnames(args, list_name)
    function selectone( args, possible, error_condition, index )
         local names = {};
         local value = nil;
         local i = 1;
         local selected = '';
         local last;
         local error_list = {};
          
          
         if index ~= nil then index = tostring(index); end
         while true do
       
             last = selectone( args, cfg.aliases[list_name .. '-Last'], 'redundant_parameters', i );
        -- Handle special case of "#" replaced by empty string
             if not is_set(last) then
        if index == '1' then
                 -- just in case someone passed in an empty parameter
             for _, v in ipairs( possible ) do
                 break;
                v = v:gsub( "#", "" );
                if is_set(args[v]) then
                    if value ~= nil and selected ~= v then
                        table.insert( error_list, v );
                    else
                        value = args[v];
                        selected = v;
                    end
                end
            end       
        end
       
        for _, v in ipairs( possible ) do
            if index ~= nil then
                v = v:gsub( "#", index );
            end
             if is_set(args[v]) then
                 if value ~= nil and selected ~=  v then
                    table.insert( error_list, v );
                 else
                    value = args[v];
                    selected = v;
                end
             end
             end
            names[i] = {
                last = last,
                first = selectone( args, cfg.aliases[list_name .. '-First'], 'redundant_parameters', i ),
                link = selectone( args, cfg.aliases[list_name .. '-Link'], 'redundant_parameters', i ),
                mask = selectone( args, cfg.aliases[list_name .. '-Mask'], 'redundant_parameters', i )
            };
            i = i + 1;
         end
         end
          
         return names;
         if #error_list > 0 then
    end
            local error_str = "";
     
            for _, k in ipairs( error_list ) do
    -- Populates ID table from arguments using configuration settings
                if error_str ~= "" then error_str = error_str .. cfg.messages['parameter-separator'] end
    function extractids( args )
                error_str = error_str .. wrap( 'parameter', k );
         local id_list = {};
            end
        for k, v in pairs( cfg.id_handlers ) do  
             if #error_list > 1 then
            v = selectone( args, v.parameters, 'redundant_parameters' );
                error_str = error_str .. cfg.messages['parameter-final-separator'];
             if is_set(v) then id_list[k] = v; end
            else
                error_str = error_str .. cfg.messages['parameter-pair-separator'];
            end
            error_str = error_str .. wrap( 'parameter', selected );
            table.insert( z.message_tail, { seterror( error_condition, {error_str}, true ) } );
         end
         end
       
         return id_list;
         return value, selected;
    end
    end


    -- COinS metadata (see <http://ocoins.info/>) allows automated tools to parse
    -- Takes a table of IDs and turns it into a table of formatted ID outputs.
    -- the citation information.
    function buildidlist( id_list, options )
    function COinS(data)
         local new_list, handler = {};
         if 'table' ~= type(data) or nil == next(data) then
            return '';
        end
          
          
         local ctx_ver = "Z39.88-2004";
         function fallback(k) return { __index = function(t,i) return cfg.id_handlers[k][i] end } end;
          
          
         -- treat table strictly as an array with only set values.
         for k, v in pairs( id_list ) do
        local OCinSoutput = setmetatable( {}, {
            -- fallback to read-only cfg
             __newindex = function(self, key, value)
            handler = setmetatable( { ['id'] = v }, fallback(k) );
                 if is_set(value) then
           
                    rawset( self, #self+1, table.concat{ key, '=', mw.uri.encode( removewikilink( value ) ) } );
            if handler.mode == 'external' then
                table.insert( new_list, {handler.label, externallinkid( handler ) } );
             elseif handler.mode == 'internal' then
                table.insert( new_list, {handler.label, internallinkid( handler ) } );
            elseif handler.mode ~= 'manual' then
                 error( cfg.messages['unknown_ID_mode'] );
            elseif k == 'DOI' then
                table.insert( new_list, {handler.label, doi( v, options.DoiBroken ) } );
            elseif k == 'ASIN' then
                table.insert( new_list, {handler.label, amazon( v, options.ASINTLD ) } );
            elseif k == 'OL' then
                table.insert( new_list, {handler.label, openlibrary( v ) } );
            elseif k == 'PMC' then
                table.insert( new_list, {handler.label, pmc( v, options.Embargo ) } );
            elseif k == 'ISSN' then
            table.insert( new_list, {handler.label, issn( v ) } );
            elseif k == 'ISBN' then
                local ISBN = internallinkid( handler );
                if not checkisbn( v ) and not is_set(options.IgnoreISBN) then
                    ISBN = ISBN .. seterror( 'bad_isbn', {}, false, " ", "" );
                 end
                 end
                table.insert( new_list, {handler.label, ISBN } );               
            else
                error( cfg.messages['unknown_manual_ID'] );
             end
             end
         });
         end
          
          
         if is_set(data.Chapter) then
         function comp( a, b )
             OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
             return a[1] < b[1];
            OCinSoutput["rft.genre"] = "bookitem";
            OCinSoutput["rft.btitle"] = data.Chapter;
            OCinSoutput["rft.atitle"] = data.Title;
        elseif is_set(data.Periodical) then
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:journal";
            OCinSoutput["rft.genre"] = "article";
            OCinSoutput["rft.jtitle"] = data.Periodical;
            OCinSoutput["rft.atitle"] = data.Title;
        else
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
            OCinSoutput["rft.genre"] = "book"
            OCinSoutput["rft.btitle"] = data.Title;
         end
         end
          
          
         OCinSoutput["rft.place"] = data.PublicationPlace;
         table.sort( new_list, comp );
         OCinSoutput["rft.date"] = data.Date;
         for k, v in ipairs( new_list ) do
        OCinSoutput["rft.series"] = data.Series;
            new_list[k] = v[2];
        OCinSoutput["rft.volume"] = data.Volume;
         end
         OCinSoutput["rft.issue"] = data.Issue;
        OCinSoutput["rft.pages"] = data.Pages;
        OCinSoutput["rft.edition"] = data.Edition;
        OCinSoutput["rft.pub"] = data.PublisherName;
          
          
         for k, v in pairs( data.ID_list ) do
         return new_list;
            local id, value = cfg.id_handlers[k].COinS;
    end
            if k == 'ISBN' then value = cleanisbn( v ); else value = v; end
     
            if string.sub( id or "", 1, 4 ) == 'info' then
    -- Chooses one matching parameter from a list of parameters to consider
                OCinSoutput["rft_id"] = table.concat{ id, "/", v };
    -- Generates an error if more than one match is present.
            else
    function selectone( args, possible, error_condition, index )
                OCinSoutput[ id ] = value;
        local value = nil;
            end
        local selected = '';
        end
        local error_list = {};
       
        if index ~= nil then index = tostring(index); end
          
          
         local last, first;
         -- Handle special case of "#" replaced by empty string
         for k, v in ipairs( data.Authors ) do
         if index == '1' then
            last, first = v.last, v.first;
            for _, v in ipairs( possible ) do
            if k == 1 then
                v = v:gsub( "#", "" );
                if is_set(last) then
                if is_set(args[v]) then
                     OCinSoutput["rft.aulast"] = last;
                    if value ~= nil and selected ~= v then
                        table.insert( error_list, v );
                     else
                        value = args[v];
                        selected = v;
                    end
                 end
                 end
                if is_set(first) then
             end      
                    OCinSoutput["rft.aufirst"] = first;
                end
            end
            if is_set(last) and is_set(first) then
                OCinSoutput["rft.au"] = table.concat{ last, ", ", first };
            elseif is_set(last) then
                OCinSoutput["rft.au"] = last;
             end
         end
         end
          
          
         OCinSoutput.rft_id = data.URL;
         for _, v in ipairs( possible ) do
        OCinSoutput.rfr_id = table.concat{ "info:sid/", mw.site.server:match( "[^/]*$" ), ":", data.RawPage };
            if index ~= nil then
        OCinSoutput = setmetatable( OCinSoutput, nil );
                v = v:gsub( "#", index );
            end
            if is_set(args[v]) then
                if value ~= nil and selected ~=  v then
                    table.insert( error_list, v );
                else
                    value = args[v];
                    selected = v;
                end
            end
        end
       
        if #error_list > 0 then
            local error_str = "";
            for _, k in ipairs( error_list ) do
                if error_str ~= "" then error_str = error_str .. cfg.messages['parameter-separator'] end
                error_str = error_str .. wrap( 'parameter', k );
            end
            if #error_list > 1 then
                error_str = error_str .. cfg.messages['parameter-final-separator'];
            else
                error_str = error_str .. cfg.messages['parameter-pair-separator'];
            end
            error_str = error_str .. wrap( 'parameter', selected );
            table.insert( z.message_tail, { seterror( error_condition, {error_str}, true ) } );
        end
          
          
        -- sort with version string always first, and combine.
         return value, selected;
        table.sort( OCinSoutput );
        table.insert( OCinSoutput, 1, "ctx_ver=" .. ctx_ver );  -- such as "Z39.88-2004"
         return table.concat(OCinSoutput, "&");
    end
    end


    --[[
    -- COinS metadata (see <http://ocoins.info/>) allows automated tools to parse
    This is the main function foing the majority of the citation
    -- the citation information.
    formatting.
    function COinS(data)
    ]]
         if 'table' ~= type(data) or nil == next(data) then
    function citation0( config, args)
            return '';
         --[[
         end
        Load Input Parameters
          
        The argment_wrapper facillitates the mapping of multiple
         local ctx_ver = "Z39.88-2004";
        aliases to single internal variable.
        ]]
        local A = argument_wrapper( args );
     
         local i
         local PPrefix = A['PPrefix']
         local PPPrefix = A['PPPrefix']
        if is_set( A['NoPP'] ) then PPPrefix = "" PPrefix = "" end
          
          
         -- Pick out the relevant fields from the arguments. Different citation templates
         -- treat table strictly as an array with only set values.
        -- define different field names for the same underlying things.   
         local OCinSoutput = setmetatable( {}, {
         local Authors = A['Authors'];
            __newindex = function(self, key, value)
        local a = extractnames( args, 'AuthorList' );
                if is_set(value) then
     
                    rawset( self, #self+1, table.concat{ key, '=', mw.uri.encode( removewikilink( value ) ) } );
        local Coauthors = A['Coauthors'];
                end
         local Others = A['Others'];
            end
         local Editors = A['Editors'];
         });
         local e = extractnames( args, 'EditorList' );
          
     
         if is_set(data.Chapter) then
        local Year = A['Year'];
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
        local PublicationDate = A['PublicationDate'];
            OCinSoutput["rft.genre"] = "bookitem";
        local OrigYear = A['OrigYear'];
            OCinSoutput["rft.btitle"] = data.Chapter;
        local Date = A['Date'];
            OCinSoutput["rft.atitle"] = data.Title;
        local LayDate = A['LayDate'];
         elseif is_set(data.Periodical) then
        ------------------------------------------------- Get title data
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:journal";
        local Title = A['Title'];
            OCinSoutput["rft.genre"] = "article";
        local BookTitle = A['BookTitle'];
            OCinSoutput["rft.jtitle"] = data.Periodical;
        local Conference = A['Conference'];
            OCinSoutput["rft.atitle"] = data.Title;
         local TransTitle = A['TransTitle'];
         else
        local TitleNote = A['TitleNote'];
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
        local TitleLink = A['TitleLink'];
            OCinSoutput["rft.genre"] = "book"
        local Chapter = A['Chapter'];
            OCinSoutput["rft.btitle"] = data.Title;
        local ChapterLink = A['ChapterLink'];
         end
        local TransChapter = A['TransChapter'];
        local TitleType = A['TitleType'];
        local ArchiveURL = A['ArchiveURL'];
         local URL = A['URL']
        local URLorigin = A:ORIGIN('URL');
        local ChapterURL = A['ChapterURL'];
        local ChapterURLorigin = A:ORIGIN('ChapterURL');
        local ConferenceURL = A['ConferenceURL'];
        local ConferenceURLorigin = A:ORIGIN('ConferenceURL');
         local Periodical = A['Periodical'];
          
          
         if ( config.CitationClass == "encyclopaedia" ) then
         OCinSoutput["rft.place"] = data.PublicationPlace;
             if not is_set(Chapter) then
        OCinSoutput["rft.date"] = data.Date;
                 if not is_set(Title) then
        OCinSoutput["rft.series"] = data.Series;
                     Title = Periodical;
        OCinSoutput["rft.volume"] = data.Volume;
                     Periodical = '';
        OCinSoutput["rft.issue"] = data.Issue;
                 else
        OCinSoutput["rft.pages"] = data.Pages;
                    Chapter = Title
        OCinSoutput["rft.edition"] = data.Edition;
                    TransChapter = TransTitle
        OCinSoutput["rft.pub"] = data.PublisherName;
                    Title = '';
       
                    TransTitle = '';
        for k, v in pairs( data.ID_list ) do
                end
            local id, value = cfg.id_handlers[k].COinS;
            if k == 'ISBN' then value = cleanisbn( v ); else value = v; end
            if string.sub( id or "", 1, 4 ) == 'info' then
                OCinSoutput["rft_id"] = table.concat{ id, "/", v };
            else
                OCinSoutput[ id ] = value;
             end
        end
       
        local last, first;
        for k, v in ipairs( data.Authors ) do
            last, first = v.last, v.first;
            if k == 1 then
                 if is_set(last) then
                     OCinSoutput["rft.aulast"] = last;
                end
                if is_set(first) then
                     OCinSoutput["rft.aufirst"] = first;
                 end
            end
            if is_set(last) and is_set(first) then
                OCinSoutput["rft.au"] = table.concat{ last, ", ", first };
            elseif is_set(last) then
                OCinSoutput["rft.au"] = last;
             end
             end
         end
         end
        local Series = A['Series'];
        local Volume = A['Volume'];
        local Issue = A['Issue'];
        local Position = '';
        local Page, Pages, At, page_type;
          
          
         Page = A['Page'];
         OCinSoutput.rft_id = data.URL;
         Pages = hyphentodash( A['Pages'] );
         OCinSoutput.rfr_id = table.concat{ "info:sid/", mw.site.server:match( "[^/]*$" ), ":", data.RawPage };
         At = A['At'];
         OCinSoutput = setmetatable( OCinSoutput, nil );
          
          
         if is_set(Page) then
         -- sort with version string always first, and combine.
            if is_set(Pages) or is_set(At) then
        table.sort( OCinSoutput );
                Page = Page .. " " .. seterror('extra_pages');
        table.insert( OCinSoutput, 1, "ctx_ver=" .. ctx_ver );  -- such as "Z39.88-2004"
                Pages = '';
        return table.concat(OCinSoutput, "&");
                At = '';
    end
            end
     
        elseif is_set(Pages) then
    --[[
            if is_set(At) then
    This is the main function foing the majority of the citation
                Pages = Pages .. " " .. seterror('extra_pages');
    formatting.
                At = '';
    ]]
            end
    function citation0( config, args)
        end   
        --[[
          
        Load Input Parameters
         local Edition = A['Edition'];
        The argment_wrapper facillitates the mapping of multiple
         local PublicationPlace = A['PublicationPlace']
        aliases to single internal variable.
         local Place = A['Place'];
        ]]
        local A = argument_wrapper( args );
     
         local i
         local PPrefix = A['PPrefix']
         local PPPrefix = A['PPPrefix']
         if is_set( A['NoPP'] ) then PPPrefix = "" PPrefix = "" end
          
          
         if not is_set(PublicationPlace) and is_set(Place) then
         -- Pick out the relevant fields from the arguments.  Different citation templates
            PublicationPlace = Place;
         -- define different field names for the same underlying things.   
        end
         local Authors = A['Authors'];
       
         local a = extractnames( args, 'AuthorList' );
        if PublicationPlace == Place then Place = ''; end
     
          
         local Coauthors = A['Coauthors'];
         local PublisherName = A['PublisherName'];
         local Others = A['Others'];
         local RegistrationRequired = A['RegistrationRequired'];
         local Editors = A['Editors'];
        local SubscriptionRequired = A['SubscriptionRequired'];
         local e = extractnames( args, 'EditorList' );
         local Via = A['Via'];
         local AccessDate = A['AccessDate'];
         local ArchiveDate = A['ArchiveDate'];
         local Agency = A['Agency'];
        local DeadURL = A['DeadURL']
        local Language = A['Language'];
        local Format = A['Format'];
        local Ref = A['Ref'];
       
        local DoiBroken = A['DoiBroken'];
        local ID = A['ID'];
        local ASINTLD = A['ASINTLD'];
        local IgnoreISBN = A['IgnoreISBN'];
        local Embargo = A['Embargo'];


         local ID_list = extractids( args );
         local Year = A['Year'];
          
        local PublicationDate = A['PublicationDate'];
         local Quote = A['Quote'];
        local OrigYear = A['OrigYear'];
         local PostScript = A['PostScript'];
        local Date = A['Date'];
         local LayURL = A['LayURL'];
        local LayDate = A['LayDate'];
         local LaySource = A['LaySource'];
        ------------------------------------------------- Get title data
         local Transcript = A['Transcript'];
        local Title = A['Title'];
         local TranscriptURL = A['TranscriptURL']  
         local BookTitle = A['BookTitle'];
         local TranscriptURLorigin = A:ORIGIN('TranscriptURL');
         local Conference = A['Conference'];
         local sepc = A['Separator'];
         local TransTitle = A['TransTitle'];
         local LastAuthorAmp = A['LastAuthorAmp'];
         local TitleNote = A['TitleNote'];
         local no_tracking_cats = A['NoTracking'];
         local TitleLink = A['TitleLink'];
     
         local Chapter = A['Chapter'];
         local use_lowercase = ( sepc ~= '.' );
         local ChapterLink = A['ChapterLink'];
         local this_page = mw.title.getCurrentTitle(); --Also used for COinS and for language
         local TransChapter = A['TransChapter'];
        local TitleType = A['TitleType'];
         local Degree = A['Degree'];
         local Docket = A['Docket'];
         local ArchiveURL = A['ArchiveURL'];
        local URL = A['URL']
         local URLorigin = A:ORIGIN('URL');
        local ChapterURL = A['ChapterURL'];
        local ChapterURLorigin = A:ORIGIN('ChapterURL');
         local ConferenceURL = A['ConferenceURL'];
        local ConferenceURLorigin = A:ORIGIN('ConferenceURL');
        local Periodical = A['Periodical'];
          
          
         if not is_set(no_tracking_cats) then
         if ( config.CitationClass == "encyclopaedia" ) then
            for k, v in pairs( cfg.uncategorized_namespaces ) do
            if not is_set(Chapter) then
                 if this_page.nsText == v then
                if not is_set(Title) then
                     no_tracking_cats = "true";
                    Title = Periodical;
                     break;
                    Periodical = '';
                 else
                    Chapter = Title
                    TransChapter = TransTitle
                     Title = '';
                     TransTitle = '';
                 end
                 end
             end
             end
         end
         end


         -- At this point fields may be nil if they weren't specified in the template use.  We can use that fact.
         local Series = A['Series'];
        local Volume = A['Volume'];
        local Issue = A['Issue'];
        local Position = '';
        local Page, Pages, At, page_type;
       
        Page = A['Page'];
        Pages = hyphentodash( A['Pages'] );
        At = A['At'];
          
          
        -- Account for the oddity that is {{cite conference}}, before generation of COinS data.
         if is_set(Page) then
         if is_set(BookTitle) then
             if is_set(Pages) or is_set(At) then
             Chapter = Title;
                Page = Page .. " " .. seterror('extra_pages');
            ChapterLink = TitleLink;
                Pages = '';
            TransChapter = TransTitle;
                At = '';
            Title = BookTitle;
            end
            TitleLink = '';
         elseif is_set(Pages) then
            TransTitle = '';
            if is_set(At) then
        end
                Pages = Pages .. " " .. seterror('extra_pages');
         -- Account for the oddity that is {{cite episode}}, before generation of COinS data.
                At = '';
        if config.CitationClass == "episode" then
             end
            local AirDate = A['AirDate'];
        end   
            local SeriesLink = A['SeriesLink'];
       
             local Season = A['Season'];
        local Edition = A['Edition'];
            local SeriesNumber = A['SeriesNumber'];
        local PublicationPlace = A['PublicationPlace']
            local Network = A['Network'];
        local Place = A['Place'];
            local Station = A['Station'];
       
            local s, n = {}, {};
        if not is_set(PublicationPlace) and is_set(Place) then
            local Sep = (first_set(A["SeriesSeparator"], A["Separator"]) or "") .. " ";
             PublicationPlace = Place;
           
            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(Station) then table.insert(n, Station); end
           
            Date = Date or AirDate;
            Chapter = Title;
            ChapterLink = TitleLink;
            TransChapter = TransTitle;
            Title = Series;
            TitleLink = SeriesLink;
            TransTitle = '';
           
            Series = table.concat(s, Sep);
             ID = table.concat(n, Sep);
         end
         end
          
          
         -- COinS metadata (see <http://ocoins.info/>) for
         if PublicationPlace == Place then Place = ''; end
         -- automated parsing of citation information.
       
         local OCinSoutput = COinS{
        local PublisherName = A['PublisherName'];
            ['Periodical'] = Periodical,
        local RegistrationRequired = A['RegistrationRequired'];
            ['Chapter'] = Chapter,
         local SubscriptionRequired = A['SubscriptionRequired'];
            ['Title'] = Title,
         local Via = A['Via'];
            ['PublicationPlace'] = PublicationPlace,
        local AccessDate = A['AccessDate'];
            ['Date'] = first_set(Date, Year, PublicationDate),
        local ArchiveDate = A['ArchiveDate'];
            ['Series'] = Series,
        local Agency = A['Agency'];
            ['Volume'] = Volume,
        local DeadURL = A['DeadURL']
            ['Issue'] = Issue,
        local Language = A['Language'];
            ['Pages'] = first_set(Page, Pages, At),
        local Format = A['Format'];
            ['Edition'] = Edition,
        local Ref = A['Ref'];
            ['PublisherName'] = PublisherName,
       
            ['URL'] = first_set( URL, ChapterURL ),
        local DoiBroken = A['DoiBroken'];
            ['Authors'] = a,
            ['ID_list'] = ID_list,
    -- Special case for cite techreport.
            ['RawPage'] = this_page.prefixedText,
    local ID = A['ID'];
        };
    if (config.CitationClass == "techreport") then -- special case for cite techreport
    if is_set(Issue) then -- cite techreport uses 'number', which everything else aliases to 'issue'
    if not is_set(ID) then -- can we use ID for the "number"?
    ID = Issue; -- yes, use it
    Issue = ""; -- unset Issue so that "number" isn't duplicated in the rendered citation or COinS metadata
    else -- can't use ID so emit error message
    ID = ID .. " " .. seterror('redundant_parameters', '<code>&#124;id=</code> and <code>&#124;number=</code>');
    end
    end
    end
        local ASINTLD = A['ASINTLD'];
        local IgnoreISBN = A['IgnoreISBN'];
        local Embargo = A['Embargo'];
     
        local ID_list = extractids( args );
       
        local Quote = A['Quote'];
        local PostScript = A['PostScript'];
        local LayURL = A['LayURL'];
        local LaySource = A['LaySource'];
        local Transcript = A['Transcript'];
        local TranscriptURL = A['TranscriptURL']  
        local TranscriptURLorigin = A:ORIGIN('TranscriptURL');
        local sepc = A['Separator'];
        local LastAuthorAmp = A['LastAuthorAmp'];
        local no_tracking_cats = A['NoTracking'];


         if is_set(Periodical) and not is_set(Chapter) and is_set(Title) then
         local use_lowercase = ( sepc ~= '.' );
            Chapter = Title;
        local this_page = mw.title.getCurrentTitle();  --Also used for COinS and for language
             ChapterLink = TitleLink;
       
            TransChapter = TransTitle;
        if not is_set(no_tracking_cats) then
            Title = '';
            for k, v in pairs( cfg.uncategorized_namespaces ) do
            TitleLink = '';
                if this_page.nsText == v then
            TransTitle = '';
                    no_tracking_cats = "true";
        end
                    break;
                end
             end
        end
     
    local anchor_year; -- used in the CITEREF identifier
    local COinS_date; -- used in the COinS metadata
     
    -- Go test all of the date-holding parameters for valid MOS:DATE format and make sure that dates are real dates.
    -- TODO: 2013-10-27: AirDate is nil when dates() is called because it hasn't been set yet.  Move the call to dates() or set AirDate earlier.
    anchor_year, COinS_date = dates({['accessdate']=AccessDate, ['airdate']=AirDate, ['archivedate']=ArchiveDate, ['date']=Date, ['doi_brokendate']=DoiBroken,
    ['embargo']=Embargo, ['laydate']=LayDate, ['publicationdate']=PublicationDate, ['year']=Year});
     
    if not is_set(Year) then -- prevent Year from being set from DateIn  TODO: eliminate the need for this?
    if is_set(anchor_year) then
    Year = anchor_year;
    end
    end


        -- Now perform various field substitutions.
    -- At this point fields may be nil if they weren't specified in the template use.  We can use that fact.
         -- We also add leading spaces and surrounding markup and punctuation to the
          
         -- various parts of the citation, but only when they are non-nil.
         -- Account for the oddity that is {{cite conference}}, before generation of COinS data.
         if not is_set(Authors) then
         if is_set(BookTitle) then
             local Maximum = tonumber( A['DisplayAuthors'] );
             Chapter = Title;
              
             ChapterLink = TitleLink;
            -- Preserve old-style implicit et al.
             TransChapter = TransTitle;
            if not is_set(Maximum) and #a == 9 then
             Title = BookTitle;
                Maximum = 8;
             TitleLink = '';
                table.insert( z.message_tail, { seterror('implict_etal_author', {}, true ) } );
            TransTitle = '';
             elseif not is_set(Maximum) then
                Maximum = #a + 1;
             end
               
            local control = {
                sep = A["AuthorSeparator"] .. " ",
                namesep = (first_set(A["AuthorNameSeparator"], A["NameSeparator"]) or "") .. " ",
                format = A["AuthorFormat"],
                maximum = Maximum,
                lastauthoramp = LastAuthorAmp
            };
              
            -- If the coauthor field is also used, prevent ampersand and et al. formatting.
            if is_set(Coauthors) then
                control.lastauthoramp = nil;
                control.maximum = #a + 1;
            end
           
            Authors = listpeople(control, a)
         end
         end
     
        -- Account for the oddity that is {{cite episode}}, before generation of COinS data.
    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 config.CitationClass == "episode" then
    table.insert( z.message_tail, { seterror('coauthors_missing_author', {}, true) } ); -- emit error message
             local AirDate = A['AirDate'];
    end
             local SeriesLink = A['SeriesLink'];
     
             local Season = A['Season'];
        local EditorCount
            local SeriesNumber = A['SeriesNumber'];
         if not is_set(Editors) then
            local Network = A['Network'];
             local Maximum = tonumber( A['DisplayEditors'] );
            local Station = A['Station'];
             -- Preserve old-style implicit et al.
            local s, n = {}, {};
             if not is_set(Maximum) and #e == 4 then
             local Sep = (first_set(A["SeriesSeparator"], A["Separator"]) or "") .. " ";
                Maximum = 3;
              
                table.insert( z.message_tail, { seterror('implict_etal_editor', {}, true) } );
             if is_set(Issue) then table.insert(s, cfg.messages["episode"] .. " " .. Issue); Issue = ''; end
             elseif not is_set(Maximum) then
            if is_set(Season) then table.insert(s, cfg.messages["season"] .. " " .. Season); end
                Maximum = #e + 1;
            if is_set(SeriesNumber) then table.insert(s, cfg.messages["series"] .. " " .. SeriesNumber); end
             end
            if is_set(Network) then table.insert(n, Network); end
     
            if is_set(Station) then table.insert(n, Station); end
             local control = {
           
                sep = A["EditorSeparator"] .. " ",
            Date = Date or AirDate;
                namesep = (first_set(A["EditorNameSeparator"], A["NameSeparator"]) or "") .. " ",
            Chapter = Title;
                format = A['EditorFormat'],
            ChapterLink = TitleLink;
                maximum = Maximum,
            TransChapter = TransTitle;
                lastauthoramp = LastAuthorAmp
            Title = Series;
             };
            TitleLink = SeriesLink;
     
             TransTitle = '';
             Editors, EditorCount = listpeople(control, e);
           
        else
             Series = table.concat(s, Sep);
             EditorCount = 1;
             ID = table.concat(n, Sep);
         end
         end
     
       
         local Cartography = "";
         -- COinS metadata (see <http://ocoins.info/>) for
         local Scale = "";
         -- automated parsing of citation information.
         if config.CitationClass == "map" then
         local OCinSoutput = COinS{
             if not is_set( Authors ) and is_set( PublisherName ) then
             ['Periodical'] = Periodical,
                Authors = PublisherName;
            ['Chapter'] = Chapter,
                PublisherName = "";
             ['Title'] = Title,
             end
             ['PublicationPlace'] = PublicationPlace,
             Cartography = A['Cartography'];
             ['Date'] = first_set(COinS_date, Date, Year, PublicationDate),
             if is_set( Cartography ) then
            ['Series'] = Series,
                Cartography = sepc .. " " .. wrap( 'cartography', Cartography, use_lowercase );
             ['Volume'] = Volume,
             end       
             ['Issue'] = Issue,
             Scale = A['Scale'];
             ['Pages'] = first_set(Page, Pages, At),
             if is_set( Scale ) then
            ['Edition'] = Edition,
                Scale = sepc .. " " .. Scale;
             ['PublisherName'] = PublisherName,
             end       
            ['URL'] = first_set( URL, ChapterURL ),
        end
             ['Authors'] = a,
       
             ['ID_list'] = ID_list,
        if not is_set(Date) then
            ['RawPage'] = this_page.prefixedText,
             Date = Year;
         };
             if is_set(Date) then
     
                local Month = A['Month'];
         if is_set(Periodical) and not is_set(Chapter) and is_set(Title) then
                if is_set(Month) then
             Chapter = Title;
                    Date = Month .. " " .. Date;
            ChapterLink = TitleLink;
                    local Day = A['Day']
            TransChapter = TransTitle;
                    if is_set(Day) then Date = Day .. " " .. Date end
            Title = '';
                end
            TitleLink = '';
            end
             TransTitle = '';
         end
       
         if inArray(PublicationDate, {Date, Year}) then PublicationDate = ''; end
        if not is_set(Date) and is_set(PublicationDate) then
             Date = PublicationDate;
             PublicationDate = '';
         end
         end


         -- Captures the value for Date prior to adding parens or other textual transformations
         -- Now perform various field substitutions.
         local DateIn = Date;
         -- We also add leading spaces and surrounding markup and punctuation to the
          
         -- various parts of the citation, but only when they are non-nil.
         if not is_set(URL) and
         if not is_set(Authors) then
             not is_set(ChapterURL) and
             local Maximum = tonumber( A['DisplayAuthors'] );
            not is_set(ArchiveURL) and
            not is_set(ConferenceURL) and
            not is_set(TranscriptURL) then
              
              
             -- Test if cite web is called without giving a URL
             -- Preserve old-style implicit et al.
             if ( config.CitationClass == "web" ) then
             if not is_set(Maximum) and #a == 9 then  
                 table.insert( z.message_tail, { seterror( 'cite_web_url', {}, true ) } );
                Maximum = 8;
                 table.insert( z.message_tail, { seterror('implict_etal_author', {}, true ) } );
            elseif not is_set(Maximum) then
                Maximum = #a + 1;
             end
             end
               
            local control = {
                sep = A["AuthorSeparator"] .. " ",
                namesep = (first_set(A["AuthorNameSeparator"], A["NameSeparator"]) or "") .. " ",
                format = A["AuthorFormat"],
                maximum = Maximum,
                lastauthoramp = LastAuthorAmp
            };
              
              
             -- Test if accessdate is given without giving a URL
             -- If the coauthor field is also used, prevent ampersand and et al. formatting.
             if is_set(AccessDate) then
             if is_set(Coauthors) then
                 table.insert( z.message_tail, { seterror( 'accessdate_missing_url', {}, true ) } );
                 control.lastauthoramp = nil;
                AccessDate = '';
                control.maximum = #a + 1;
                deprecated_parameter(); -- |coauthor= and |coathors= are deprecated; add this page to deprecated parameter category
             end
             end
              
              
             -- Test if format is given without giving a URL
             Authors = listpeople(control, a)  
            if is_set(Format) then
                Format = Format .. seterror( 'format_missing_url' );
            end
         end
         end
       
     
        -- Test if citation has no title
    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(Chapter) and
    table.insert( z.message_tail, { seterror('coauthors_missing_author', {}, true) } ); -- emit error message
            not is_set(Title) and
    deprecated_parameter(); -- |coauthor= and |coathors= are deprecated; add this page to deprecated parameter category
            not is_set(Periodical) and
    end
            not is_set(Conference) and
     
            not is_set(TransTitle) and
         local EditorCount
            not is_set(TransChapter) then
         if not is_set(Editors) then
            table.insert( z.message_tail, { seterror( 'citation_missing_title', {}, true ) } );
            local Maximum = tonumber( A['DisplayEditors'] );
        end
            -- Preserve old-style implicit et al.
          
            if not is_set(Maximum) and #e == 4 then  
         Format = is_set(Format) and " (" .. Format .. ")" or "";
                 Maximum = 3;
       
                 table.insert( z.message_tail, { seterror('implict_etal_editor', {}, true) } );
        local OriginalURL = URL
            elseif not is_set(Maximum) then
        DeadURL = DeadURL:lower();
                Maximum = #e + 1;
        if is_set( ArchiveURL ) then
            if ( DeadURL ~= "no" ) then
                 URL = ArchiveURL
                 URLorigin = A:ORIGIN('ArchiveURL')
             end
             end
            local control = {
                sep = A["EditorSeparator"] .. " ",
                namesep = (first_set(A["EditorNameSeparator"], A["NameSeparator"]) or "") .. " ",
                format = A['EditorFormat'],
                maximum = Maximum,
                lastauthoramp = LastAuthorAmp
            };
            Editors, EditorCount = listpeople(control, e);
        else
            EditorCount = 1;
         end
         end
          
     
         -- Format chapter / article title
         local Cartography = "";
         if is_set(Chapter) and is_set(ChapterLink) then  
         local Scale = "";
            Chapter = "[[" .. ChapterLink .. "|" .. Chapter .. "]]";
         if config.CitationClass == "map" then
        end
            if not is_set( Authors ) and is_set( PublisherName ) then
        if is_set(Periodical) and is_set(Title) then
                Authors = PublisherName;
            Chapter = wrap( 'italic-title', Chapter );
                PublisherName = "";
             TransChapter = wrap( 'trans-italic-title', TransChapter );
            end
        else
            Cartography = A['Cartography'];
             Chapter = wrap( 'quoted-title', Chapter );
            if is_set( Cartography ) then
             TransChapter = wrap( 'trans-quoted-title', TransChapter );
                Cartography = sepc .. " " .. wrap( 'cartography', Cartography, use_lowercase );
             end       
            Scale = A['Scale'];
             if is_set( Scale ) then
                Scale = sepc .. " " .. Scale;
             end       
         end
         end
          
          
         local TransError = ""
         if not is_set(Date) then
        if is_set(TransChapter) then
            Date = Year;
            if not is_set(Chapter) then
            if is_set(Date) then
                TransError = " " .. seterror( 'trans_missing_chapter' );
                local Month = A['Month'];
            else
                if is_set(Month) then
                TransChapter = " " .. TransChapter;
                deprecated_parameter(); -- |month= (and also |day=) is deprecated; add this page to deprecated parameter category
                    Date = Month .. " " .. Date;
                    local Day = A['Day']
                    if is_set(Day) then Date = Day .. " " .. Date end
                end
             end
             end
         end
         end
          
          
         Chapter = Chapter .. TransChapter;
         if inArray(PublicationDate, {Date, Year}) then PublicationDate = ''; end
        if not is_set(Date) and is_set(PublicationDate) then
            Date = PublicationDate;
            PublicationDate = '';
        end
     
        -- Captures the value for Date prior to adding parens or other textual transformations
        local DateIn = Date;
          
          
         if is_set(Chapter) then
         if not is_set(URL) and
             if not is_set(ChapterLink) then
             not is_set(ChapterURL) and
                if is_set(ChapterURL) then
            not is_set(ArchiveURL) and
                    Chapter = externallink( ChapterURL, Chapter ) .. TransError;
            not is_set(ConferenceURL) and
                    if not is_set(URL) then
            not is_set(TranscriptURL) then
                        Chapter = Chapter .. Format;
           
                        Format = "";
            -- Test if cite web is called without giving a URL
                    end
            if ( config.CitationClass == "web" ) then
                elseif is_set(URL) then
                 table.insert( z.message_tail, { seterror( 'cite_web_url', {}, true ) } );
                    Chapter = externallink( URL, Chapter ) .. TransError .. Format;
            end
                    URL = "";
              
                    Format = "";
            -- Test if accessdate is given without giving a URL
                 else
            if is_set(AccessDate) then
                    Chapter = Chapter .. TransError;
                 table.insert( z.message_tail, { seterror( 'accessdate_missing_url', {}, true ) } );
                end          
                AccessDate = '';
             elseif is_set(ChapterURL) then
             end
                 Chapter = Chapter .. " " .. externallink( ChapterURL, nil, ChapterURLorigin ) ..
           
                    TransError;
            -- Test if format is given without giving a URL
             else
            if is_set(Format) then
                 Chapter = Chapter .. TransError;
                 Format = Format .. seterror( 'format_missing_url' );
             end
             end
            Chapter = Chapter .. sepc .. " " -- with end-space
        elseif is_set(ChapterURL) then
            Chapter = " " .. externallink( ChapterURL, nil, ChapterURLorigin ) .. sepc .. " ";
        end       
       
        -- Format main title.
        if is_set(TitleLink) and is_set(Title) then
            Title = "[[" .. TitleLink .. "|" .. Title .. "]]"
         end
         end
          
          
         if is_set(Periodical) then
        -- Test if citation has no title
             Title = wrap( 'quoted-title', Title );
         if not is_set(Chapter) and
             TransTitle = wrap( 'trans-quoted-title', TransTitle );
             not is_set(Title) and
        elseif inArray(config.CitationClass, {"web","news","pressrelease","conference"}) and
             not is_set(Periodical) and
                not is_set(Chapter) then
            not is_set(Conference) and
             Title = wrap( 'quoted-title', Title );
            not is_set(TransTitle) and
             TransTitle = wrap( 'trans-quoted-title', TransTitle );
             not is_set(TransChapter) then
        else
             table.insert( z.message_tail, { seterror( 'citation_missing_title', {}, true ) } );
            Title = wrap( 'italic-title', Title );
            TransTitle = wrap( 'trans-italic-title', TransTitle );
         end
         end
          
          
         TransError = "";
         Format = is_set(Format) and " (" .. Format .. ")" or "";
         if is_set(TransTitle) then
       
             if not is_set(Title) then
        local OriginalURL = URL
                 TransError = " " .. seterror( 'trans_missing_title' );
        DeadURL = DeadURL:lower();
            else
         if is_set( ArchiveURL ) then
                TransTitle = " " .. TransTitle;
             if ( DeadURL ~= "no" ) then
                 URL = ArchiveURL
                URLorigin = A:ORIGIN('ArchiveURL')
             end
             end
         end
         end
          
          
         Title = Title .. TransTitle;
         -- Format chapter / article title
       
         if is_set(Chapter) and is_set(ChapterLink) then  
         if is_set(Title) then
            Chapter = "[[" .. ChapterLink .. "|" .. Chapter .. "]]";
            if not is_set(TitleLink) and is_set(URL) then  
                Title = externallink( URL, Title ) .. TransError .. Format     
                URL = "";
                Format = "";
            else
                Title = Title .. TransError;
            end
         end
         end
       
         if is_set(Periodical) and is_set(Title) then
         if is_set(Place) then
             Chapter = wrap( 'italic-title', Chapter );
             Place = " " .. wrap( 'written', Place, use_lowercase ) .. sepc .. " ";
            TransChapter = wrap( 'trans-italic-title', TransChapter );
        else
            Chapter = wrap( 'quoted-title', Chapter );
            TransChapter = wrap( 'trans-quoted-title', TransChapter );
         end
         end
          
          
         if is_set(Conference) then
        local TransError = ""
             if is_set(ConferenceURL) then
         if is_set(TransChapter) then
                 Conference = externallink( ConferenceURL, Conference );
             if not is_set(Chapter) then
                 TransError = " " .. seterror( 'trans_missing_chapter' );
            else
                TransChapter = " " .. TransChapter;
             end
             end
            Conference = sepc .. " " .. Conference
        elseif is_set(ConferenceURL) then
            Conference = sepc .. " " .. externallink( ConferenceURL, nil, ConferenceURLorigin );
         end
         end
          
          
         if not is_set(Position) then
         Chapter = Chapter .. TransChapter;
            local Minutes = A['Minutes'];
       
            if is_set(Minutes) then
        if is_set(Chapter) then
                 Position = " " .. Minutes .. " " .. cfg.messages['minutes'];
            if not is_set(ChapterLink) then
                if is_set(ChapterURL) then
                    Chapter = externallink( ChapterURL, Chapter ) .. TransError;
                    if not is_set(URL) then
                        Chapter = Chapter .. Format;
                        Format = "";
                    end
                 elseif is_set(URL) then
                    Chapter = externallink( URL, Chapter ) .. TransError .. Format;
                    URL = "";
                    Format = "";
                else
                    Chapter = Chapter .. TransError;
                end           
            elseif is_set(ChapterURL) then
                Chapter = Chapter .. " " .. externallink( ChapterURL, nil, ChapterURLorigin ) ..  
                    TransError;
             else
             else
                 local Time = A['Time'];
                 Chapter = Chapter .. TransError;
                if is_set(Time) then
                    local TimeCaption = A['TimeCaption']
                    if not is_set(TimeCaption) then
                        TimeCaption = cfg.messages['event'];
                        if sepc ~= '.' then
                            TimeCaption = TimeCaption:lower();
                        end
                    end
                    Position = " " .. TimeCaption .. " " .. Time;
                end
             end
             end
            Chapter = Chapter .. sepc .. " " -- with end-space
        elseif is_set(ChapterURL) then
            Chapter = " " .. externallink( ChapterURL, nil, ChapterURLorigin ) .. sepc .. " ";
        end       
       
        -- Format main title.
        if is_set(TitleLink) and is_set(Title) then
            Title = "[[" .. TitleLink .. "|" .. Title .. "]]"
        end
       
        if is_set(Periodical) then
            Title = wrap( 'quoted-title', Title );
            TransTitle = wrap( 'trans-quoted-title', TransTitle );
        elseif inArray(config.CitationClass, {"web","news","pressrelease","conference"}) and
                not is_set(Chapter) then
            Title = wrap( 'quoted-title', Title );
            TransTitle = wrap( 'trans-quoted-title', TransTitle );
         else
         else
             Position = " " .. Position;
             Title = wrap( 'italic-title', Title );
             At = '';
             TransTitle = wrap( 'trans-italic-title', TransTitle );
         end
         end
          
          
         if not is_set(Page) then
        TransError = "";
             if is_set(Pages) then
         if is_set(TransTitle) then
                 if is_set(Periodical) and
             if not is_set(Title) then
                    not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
                 TransError = " " .. seterror( 'trans_missing_title' );
                    Pages = ": " .. Pages;
            else
                elseif tonumber(Pages) ~= nil then
                TransTitle = " " .. TransTitle;
                    Pages = sepc .." " .. PPrefix .. Pages;
                else
                    Pages = sepc .." " .. PPPrefix .. Pages;
                end
             end
             end
         else
         end
             if is_set(Periodical) and
       
                 not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
        Title = Title .. TransTitle;
                 Page = ": " .. Page;
       
        if is_set(Title) then
             if not is_set(TitleLink) and is_set(URL) then
                 Title = externallink( URL, Title ) .. TransError .. Format     
                URL = "";
                 Format = "";
             else
             else
                 Page = sepc .." " .. PPrefix .. Page;
                 Title = Title .. TransError;
             end
             end
         end
         end
          
          
         At = is_set(At) and (sepc .. " " .. At) or "";
         if is_set(Place) then
        Position = is_set(Position) and (sepc .. " " .. Position) or "";
            Place = " " .. wrap( 'written', Place, use_lowercase ) .. sepc .. " ";
         if config.CitationClass == 'map' then
        end
             local Section = A['Section'];
       
             local Inset = A['Inset'];
         if is_set(Conference) then
             if first_set( Pages, Page, At ) ~= nil or sepc ~= '.' then
             if is_set(ConferenceURL) then
                if is_set( Section ) then
                Conference = externallink( ConferenceURL, Conference );
                    Section = ", " .. wrap( 'section', Section, true );
             end
                end
             Conference = sepc .. " " .. Conference
                if is_set( Inset ) then
        elseif is_set(ConferenceURL) then
                    Inset = ", " .. wrap( 'inset', Inset, true );
            Conference = sepc .. " " .. externallink( ConferenceURL, nil, ConferenceURLorigin );
                end
        end
       
        if not is_set(Position) then
            local Minutes = A['Minutes'];
            if is_set(Minutes) then
                Position = " " .. Minutes .. " " .. cfg.messages['minutes'];
             else
             else
                 if is_set( Section ) then
                local Time = A['Time'];
                     Section = sepc .. " " .. wrap( 'section', Section, use_lowercase );
                 if is_set(Time) then
                     if is_set( Inset ) then
                     local TimeCaption = A['TimeCaption']
                         Inset = ", " .. wrap( 'inset', Inset, true );
                     if not is_set(TimeCaption) then
                         TimeCaption = cfg.messages['event'];
                        if sepc ~= '.' then
                            TimeCaption = TimeCaption:lower();
                        end
                     end
                     end
                 elseif is_set( Inset ) then
                    Position = " " .. TimeCaption .. " " .. Time;
                     Inset = sepc .. " " .. wrap( 'inset', Inset, use_lowercase );
                end
                 end             
            end
             end             
        else
            Position = " " .. Position;
            At = '';
        end
       
        if not is_set(Page) then
            if is_set(Pages) then
                if is_set(Periodical) and
                    not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
                    Pages = ": " .. Pages;
                elseif tonumber(Pages) ~= nil then
                    Pages = sepc .." " .. PPrefix .. Pages;
                else
                    Pages = sepc .." " .. PPPrefix .. Pages;
                end
            end
        else
            if is_set(Periodical) and
                not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
                Page = ": " .. Page;
            else
                Page = sepc .." " .. PPrefix .. Page;
            end
        end
       
        At = is_set(At) and (sepc .. " " .. At) or "";
        Position = is_set(Position) and (sepc .. " " .. Position) or "";
        if config.CitationClass == 'map' then
            local Section = A['Section'];
            local Inset = A['Inset'];
            if first_set( Pages, Page, At ) ~= nil or sepc ~= '.' then
                if is_set( Section ) then
                    Section = ", " .. wrap( 'section', Section, true );
                end
                if is_set( Inset ) then
                    Inset = ", " .. wrap( 'inset', Inset, true );
                end
            else
                if is_set( Section ) then
                    Section = sepc .. " " .. wrap( 'section', Section, use_lowercase );
                    if is_set( Inset ) then
                        Inset = ", " .. wrap( 'inset', Inset, true );
                    end
                 elseif is_set( Inset ) then
                     Inset = sepc .. " " .. wrap( 'inset', Inset, use_lowercase );
                 end             
             end             
             At = At .. Section .. Inset;         
             At = At .. Section .. Inset;         
         end     
         end     
     
     
    --[[Look in the list of iso639-1 language codes to see if the value provided in the language parameter matches one of them.  If a match is found,  
    --[[Look in the list of iso639-1 language codes to see if the value provided in the language parameter matches one of them.  If a match is found,  
    use that value; if not, then use the value that was provided with the language parameter.
    use that value; if not, then use the value that was provided with the language parameter.
    Categories are assigned in a manner similar to the {{xx icon}} templates - categorizes only mainspace citations and only when the language code is not 'en' (English).
    Categories are assigned in a manner similar to the {{xx icon}} templates - categorizes only mainspace citations and only when the language code is not 'en' (English).
    ]]
    ]]
    if is_set (Language) then
    if is_set (Language) then
    local name = cfg.iso639_1[Language:lower()]; -- get the language name if Language parameter has a valid iso 639-1 code
    local name = cfg.iso639_1[Language:lower()]; -- get the language name if Language parameter has a valid iso 639-1 code
    if nil == name then
    if nil == name then
    Language=" " .. wrap( 'language', Language ); -- no match, use parameter's value
    Language=" " .. wrap( 'language', Language ); -- no match, use parameter's value
    else
    else
    if 0 == this_page.namespace and 'en' ~= Language:lower() then --found a match; is this page main / article space and English not the language?
    if 0 == this_page.namespace and 'en' ~= Language:lower() then --found a match; is this page main / article space and English not the language?
    Language=" " .. wrap( 'language', name .. '[[Category:Articles with ' .. name .. '-language external links]]' ); -- in main space and not English: categorize
    Language=" " .. wrap( 'language', name .. '[[Category:Articles with ' .. name .. '-language external links]]' ); -- in main space and not English: categorize
    else
    else
    Language=" " .. wrap( 'language', name ); --not in mainspace or language is English so don't categorize
    Language=" " .. wrap( 'language', name ); --not in mainspace or language is English so don't categorize
    end
    end
    end
    end
    else
    else
    Language=""; -- language not specified so make sure this is an empty string;
    Language=""; -- language not specified so make sure this is an empty string;
    end
     
    Others = is_set(Others) and (sepc .. " " .. Others) or "";
     
    -- handle type parameter for those CS1 citations that have default values
     
    if "pressrelease" == config.CitationClass then -- if this citation is cite press release
    if not is_set (TitleType) then
    TitleType = "Press release"; -- if type not specified, display the press release annotation
    else
    if "none" == TitleType then TitleType = ""; end -- if |type=none then type parameter not displayed
    end
     
    elseif "techreport" == config.CitationClass then -- if this citation is cite techreport
    if not is_set (TitleType) then
    TitleType = "Technical report"; -- if type not specified, display the techreport annotation
    else
    if "none" == TitleType then TitleType = ""; end -- if |type=none then type parameter not displayed; |number=, if set, will be displayed
    end
    elseif "thesis" == config.CitationClass then -- if this citation is cite thesis
    if not is_set (TitleType) then
    if is_set(Degree) then -- if type not specified, display one of the thesis annotations
    TitleType = Degree .. " thesis"; -- if a degree (masters, PhD, ...) is specified include it in the display
    else
    TitleType = "Thesis"; -- otherwise display the simple thesis annotation
    end
    else
    if "none" == TitleType then TitleType = ""; end -- if |type=none then type parameter not displayed
    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 "";
        Edition = is_set(Edition) and (" " .. wrap( 'edition', Edition )) or "";
        Issue = is_set(Issue) and (" (" .. Issue .. ")") or "";
        Series = is_set(Series) and (sepc .. " " .. Series) or "";
        OrigYear = is_set(OrigYear) and (" [" .. OrigYear .. "]") or "";
        Agency = is_set(Agency) and (sepc .. " " .. Agency) or "";
       
        if is_set(Volume) then
            if ( mw.ustring.len(Volume) > 4 )
              then Volume = sepc .." " .. Volume;
              else Volume = " <b>" .. hyphentodash(Volume) .. "</b>";
            end
        end
       
        ------------------------------------ totally unrelated data
        --[[ Loosely mimic {{subscription required}} template; Via parameter identifies a delivery source that is not the publisher; these sources often, but not always, exist
        behind a registration or paywall.  So here, we've chosen to decouple via from subscription (via has never been part of the registration required template).
       
        Subscription implies paywall; Registration does not.  If both are used in a citation, the subscription required link note is displayed. There are no error messages for this condition.
        ]]
        if is_set(Via) then
            Via = " " .. wrap( 'via', Via );
        end
     
    if is_set(SubscriptionRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['subscription']; --here when 'via' parameter not used but 'subscription' is
        elseif is_set(RegistrationRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['registration']; --here when 'via' and 'subscription' parameters not used but 'registration' is
        end
     
        if is_set(AccessDate) then
            local retrv_text = " " .. cfg.messages['retrieved']
            if (sepc ~= ".") then retrv_text = retrv_text:lower() end
            AccessDate = '<span class="reference-accessdate">' .. sepc
                .. substitute( retrv_text, {AccessDate} ) .. '</span>'
        end
       
        if is_set(ID) then ID = sepc .." ".. ID; end
      if "thesis" == config.CitationClass and is_set(Docket) then
    ID = sepc .." Docket ".. Docket .. ID;
    end
    end


        Others = is_set(Others) and (sepc .. " " .. Others) or "";
        TitleType = is_set(TitleType) and (" (" .. TitleType .. ")") or "";
        TitleNote = is_set(TitleNote) and (sepc .. " " .. TitleNote) or "";
        Edition = is_set(Edition) and (" " .. wrap( 'edition', Edition )) or "";
        Issue = is_set(Issue) and (" (" .. Issue .. ")") or "";
        Series = is_set(Series) and (sepc .. " " .. Series) or "";
        OrigYear = is_set(OrigYear) and (" [" .. OrigYear .. "]") or "";
        Agency = is_set(Agency) and (sepc .. " " .. Agency) or "";
       
        if is_set(Volume) then
            if ( mw.ustring.len(Volume) > 4 )
              then Volume = sepc .." " .. Volume;
              else Volume = " <b>" .. hyphentodash(Volume) .. "</b>";
            end
        end
       
        ------------------------------------ totally unrelated data
        --[[ Loosely mimic {{subscription required}} template; Via parameter identifies a delivery source that is not the publisher; these sources often, but not always, exist
        behind a registration or paywall.  So here, we've chosen to decouple via from subscription (via has never been part of the registration required template).
       
        Subscription implies paywall; Registration does not.  If both are used in a citation, the subscription required link note is displayed. There are no error messages for this condition.
        ]]
        if is_set(Via) then
            Via = " " .. wrap( 'via', Via );
        end
    if is_set(SubscriptionRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['subscription']; --here when 'via' parameter not used but 'subscription' is
        elseif is_set(RegistrationRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['registration']; --here when 'via' and 'subscription' parameters not used but 'registration' is
        end
        if is_set(AccessDate) then
            local retrv_text = " " .. cfg.messages['retrieved']
            if (sepc ~= ".") then retrv_text = retrv_text:lower() end
            AccessDate = '<span class="reference-accessdate">' .. sepc
                .. substitute( retrv_text, {AccessDate} ) .. '</span>'
        end
       
        if is_set(ID) then ID = sepc .." ".. ID; end
          
          
         ID_list = buildidlist( ID_list, {DoiBroken = DoiBroken, ASINTLD = ASINTLD, IgnoreISBN = IgnoreISBN, Embargo=Embargo} );
         ID_list = buildidlist( ID_list, {DoiBroken = DoiBroken, ASINTLD = ASINTLD, IgnoreISBN = IgnoreISBN, Embargo=Embargo} );
    Line 1,680: Line 1,934:
                     end
                     end
                 end
                 end
                names[ #names + 1 ] = Year;
    names[ #names + 1 ] = Year or anchor_year; -- Year first for legacy citations
                 id = anchorid(names)
                 id = anchorid(names)
             end
             end

    Revision as of 11:39, 9 November 2013

    Documentation for this module may be created at Module:Citation/CS1/doc

    local z = {
        error_categories = {};
        error_ids = {};
        message_tail = {};
    }
    
    -- Include translation message hooks, ID and error handling configuration settings.
    local cfg = mw.loadData( 'Module:Citation/CS1/Configuration/sandbox' );
    
    -- Contains a list of all recognized parameters
    local whitelist = mw.loadData( 'Module:Citation/CS1/Whitelist/sandbox' );
    
    -- Whether variable is set or not
    function is_set( var )
        return not (var == nil or var == '');
    end
    
    -- First set variable or nil if none
    function first_set(...)
        local list = {...};
        for _, var in pairs(list) do
            if is_set( var ) then
                return var;
            end
        end
    end
    
    -- Whether needle is in haystack
    function inArray( needle, haystack )
        if needle == nil then
            return false;
        end
        for n,v in ipairs( haystack ) do
            if v == needle then
                return n;
            end
        end
        return false;
    end
    
    -- Add this page to the deprecated parameter tracking category
    function deprecated_parameter()
    	if true ~= Page_in_deprecated_cat then	-- if we haven't been here before then set a 
    		Page_in_deprecated_cat=true;		-- sticky flag so that if there are more than one deprecated parameter the category is added only once
    		table.insert( z.error_categories, "Pages containing cite templates with deprecated parameters" );	-- add page to category
    	end
    end
    
    -- Populates numbered arguments in a message string using an argument table.
    function substitute( msg, args )
        return args and tostring( mw.message.newRawMessage( msg, args ) ) or msg;
    end
    
    -- Wraps a string using a message_list configuration taking one argument
    function wrap( key, str, lower )
        if not is_set( str ) then
            return "";
        elseif inArray( key, { 'italic-title', 'trans-italic-title' } ) then
            str = safeforitalics( str );
        end
        if lower == true then
            return substitute( cfg.messages[key]:lower(), {str} );
        else
            return substitute( cfg.messages[key], {str} );
        end        
    end
    
    --[[
    Argument wrapper.  This function provides support for argument 
    mapping defined in the configuration file so that multiple names
    can be transparently aliased to single internal variable.
    ]]
    function argument_wrapper( args )
        local origin = {};
        
        return setmetatable({
            ORIGIN = function( self, k )
                local dummy = self[k]; --force the variable to be loaded.
                return origin[k];
            end
        },
        {
            __index = function ( tbl, k )
                if origin[k] ~= nil then
                    return nil;
                end
                
                local args, list, v = args, cfg.aliases[k];
                
                if type( list ) == 'table' then
                    v, origin[k] = selectone( args, list, 'redundant_parameters' );
                    if origin[k] == nil then
                        origin[k] = ''; -- Empty string, not nil
                    end
                elseif list ~= nil then
                    v, origin[k] = args[list], list;
                else
                    -- maybe let through instead of raising an error?
                    -- v, origin[k] = args[k], k;
                    error( cfg.messages['unknown_argument_map'] );
                end
                
                -- Empty strings, not nil;
                if v == nil then
                    v = cfg.defaults[k] or '';
                    origin[k] = '';
                end
                
                tbl = rawset( tbl, k, v );
                return v;
            end,
        });
    end
    
    -- Checks that parameter name is valid using the whitelist
    function validate( name )
        name = tostring( name );
        
        -- Normal arguments
        if whitelist.basic_arguments[ name ] then
            return true;
        end
        
        -- Arguments with numbers in them
        name = name:gsub( "%d+", "#" );
        if whitelist.numbered_arguments[ name ] then
            return true;
        end
        
        -- Not found, argument not supported.
        return false
    end
    
    -- Formats a comment for error trapping
    function errorcomment( content, hidden )
        return wrap( hidden and 'hidden-error' or 'visible-error', content );
    end
    
    --[[
    Sets an error condition and returns the appropriate error message.  The actual placement
    of the error message in the output is the responsibility of the calling function.
    ]]
    function seterror( error_id, arguments, raw, prefix, suffix )
        local error_state = cfg.error_conditions[ error_id ];
        
        prefix = prefix or "";
        suffix = suffix or "";
        
        if error_state == nil then
            error( cfg.messages['undefined_error'] );
        elseif is_set( error_state.category ) then
            table.insert( z.error_categories, error_state.category );
        end
        
        local message = substitute( error_state.message, arguments );
        
        message = message .. " ([[" .. cfg.messages['help page link'] .. 
            "#" .. error_state.anchor .. "|" ..
            cfg.messages['help page label'] .. "]])";
        
        z.error_ids[ error_id ] = true;
        if inArray( error_id, { 'bare_url_missing_title', 'trans_missing_title' } )
                and z.error_ids['citation_missing_title'] then
            return '', false;
        end
        
        message = table.concat({ prefix, message, suffix });
        
        if raw == true then
            return message, error_state.hidden;
        end        
            
        return errorcomment( message, error_state.hidden );
    end
    
    -- Formats a wiki style external link
    function externallinkid(options)
        local url_string = options.id;
        if options.encode == true or options.encode == nil then
            url_string = mw.uri.encode( url_string );
        end
        return mw.ustring.format( '[[%s|%s]]%s[%s%s%s %s]',
            options.link, options.label, options.separator or "&nbsp;",
            options.prefix, url_string, options.suffix or "",
            mw.text.nowiki(options.id)
        );
    end
    
    -- Formats a wiki style internal link
    function internallinkid(options)
        return mw.ustring.format( '[[%s|%s]]%s[[%s%s%s|%s]]',
            options.link, options.label, options.separator or "&nbsp;",
            options.prefix, options.id, options.suffix or "",
            mw.text.nowiki(options.id)
        );
    end
    
    -- Format an external link with error checking
    function externallink( URL, label, source )
        local error_str = "";
        if not is_set( label ) then
            label = URL;
            if is_set( source ) then
                error_str = seterror( 'bare_url_missing_title', { wrap( 'parameter', source ) }, false, " " );
            else
                error( cfg.messages["bare_url_no_origin"] );
            end            
        end
        if not checkurl( URL ) then
            error_str = seterror( 'bad_url', {}, false, " " ) .. error_str;
        end
        return table.concat({ "[", URL, " ", safeforurl( label ), "]", error_str });
    end
    
    -- Formats a link to Amazon
    function amazon(id, domain)
        if not is_set(domain) then 
            domain = "com"
        elseif ( "jp" == domain or "uk" == domain ) then
            domain = "co." .. domain
        end
        local handler = cfg.id_handlers['ASIN'];
        return externallinkid({link = handler.link,
            label=handler.label , prefix="//www.amazon."..domain.."/dp/",id=id,
            encode=handler.encode, separator = handler.separator})
    end
    
    --[[
    Formats a PMC and checks for embargoed articles.  The embargo parameter takes a date for a value. If the embargo date is in the futue
    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
    the PMC identifier is linked to the article through the link at cfg.id_handlers['PMC'].link.
    
    The {{citation/core}} version of {{cite journal}} links the citation title (if url parameter is empty) when embargo date is in the past
    or when embargo parameter is missing or empty. That behavior is inconsistent with the behavior of other identifiers used in CS1 and is
    not supported here.
    ]]
    function pmc(id, embargo)
    	local handler = cfg.id_handlers['PMC'];
        
    	local text;
    	
    	if is_set(embargo) then
    		local lang = mw.getContentLanguage();
    		local good1, embargo_date, good2, todays_date;
    		good1, embargo_date = pcall( lang.formatDate, lang, 'U', embargo );
    		good2, todays_date = pcall( lang.formatDate, lang, 'U' );
    
    		if good1 and good2 and tonumber( embargo_date ) < tonumber( todays_date ) then	--if embargo date is in the past then
    			text = externallinkid({link = handler.link, label = handler.label,			--ok to link to article
    				prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode})
    		else
    			text="[[" .. handler.link .. "|" .. handler.label .. "]]:" .. handler.separator .. id;	--still embargoed so no external link
    		end
    	else
    		text = externallinkid({link = handler.link, label = handler.label,			--no embargo date, ok to link to article
    			prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode})
    	end
    	return text
    end
    
    -- Formats a DOI and checks for DOI errors.
    function doi(id, inactive)
        local cat = ""
        local handler = cfg.id_handlers['DOI'];
        
        local text;
        if is_set(inactive) then
            text = "[[" .. handler.link .. "|" .. handler.label .. "]]:" .. id;
            table.insert( z.error_categories, "Pages with DOIs inactive since " .. selectyear(inactive) );        
            inactive = " (" .. cfg.messages['inactive'] .. " " .. inactive .. ")" 
        else 
            text = externallinkid({link = handler.link, label = handler.label,
                prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode})
            inactive = "" 
        end
        if ( string.sub(id,1,3) ~= "10." ) then      
            cat = seterror( 'bad_doi' );
        end
        return text .. inactive .. cat 
    end
    
    -- Formats an OpenLibrary link, and checks for associated errors.
    function openlibrary(id)
        local code = id:sub(-1,-1)
        local handler = cfg.id_handlers['OL'];
        if ( code == "A" ) then
            return externallinkid({link=handler.link, label=handler.label,
                prefix="http://openlibrary.org/authors/OL",id=id, separator=handler.separator,
                encode = handler.encode})
        elseif ( code == "M" ) then
            return externallinkid({link=handler.link, label=handler.label,
                prefix="http://openlibrary.org/books/OL",id=id, separator=handler.separator,
                encode = handler.encode})
        elseif ( code == "W" ) then
            return externallinkid({link=handler.link, label=handler.label,
                prefix= "http://openlibrary.org/works/OL",id=id, separator=handler.separator,
                encode = handler.encode})
        else
            return externallinkid({link=handler.link, label=handler.label,
                prefix= "http://openlibrary.org/OL",id=id, separator=handler.separator,
                encode = handler.encode}) .. 
                ' ' .. seterror( 'bad_ol' );
        end
    end
    
    --[[
    Validate and format an issn.  This code fixes the case where an editor has included an ISSN in the citation but has separated the two groups of four
    digits with a space.  When that condition occurred, the resulting link looked like this:
    
    	|issn=0819 4327 gives: [http://www.worldcat.org/issn/0819 4327 0819 4327]  -- can't have spaces in an external link
    	
    This code now prevents that by inserting a hyphen at the issn midpoint.  It also validates the issn for length and makes sure that the checkdigit agrees
    with the calculated value.  Incorrect length (8 digits), characters other than 0-9 and X, or checkdigit / calculated value mismatch will all cause a check issn
    error message.  The issn is always displayed with a hyphen, even if the issn was given as a single group of 8 digits.
    ]]
    function issn(id)
    	local issn_copy = id;		-- save a copy of unadulterated issn; use this version for display if issn does not validate
    	local handler = cfg.id_handlers['ISSN'];
    	local text;
    	local valid_issn = true;
    
    	id=id:gsub( "[%s-–]", "" );									-- strip spaces, hyphens, and ndashes from the issn
    
    	if 8 ~= id:len() or nil == id:match( "^%d*X?$" ) then		-- validate the issn: 8 didgits long, containing only 0-9 or X in the last position
    		valid_issn=false;										-- wrong length or improper character
    	else
    		valid_issn=is_valid_isxn(id, 8);						-- validate issn
    	end
    
    	if true == valid_issn then
    		id = string.sub( id, 1, 4 ) .. "-" .. string.sub( id, 5 );	-- if valid, display correctly formatted version
    	else
    		id = issn_copy;											-- if not valid, use the show the invalid issn with error message
    	end
    	
    	text = externallinkid({link = handler.link, label = handler.label,
    		prefix=handler.prefix,id=id,separator=handler.separator, encode=handler.encode})
     
    	if false == valid_issn then
    		text = text .. ' ' .. seterror( 'bad_issn' )			-- add an error message if the issn is invalid
    	end 
    	
    	return text
    end
    
    -- returns a number according to the month in a date 1 for January, etc.  If not a valid month, returns 0
    function get_month_number (month)
    local long_months = {['january']=1, ['february']=2, ['march']=3, ['april']=4, ['may']=5, ['june']=6, ['july']=7, ['august']=8, ['september']=9, ['october']=10, ['november']=11, ['december']=12};
    local short_months = {['jan']=1, ['feb']=2, ['mar']=3, ['apr']=4, ['may']=5, ['jun']=6, ['jul']=7, ['aug']=8, ['sep']=9, ['oct']=10, ['nov']=11, ['dec']=12};
    local temp;
    	temp=long_months[month:lower()];
    	if temp then return temp; end	-- if month is the long-form name
    	temp=short_months[month:lower()];
    	if temp then return temp; end	-- if month is the short-form name
    	return 0;													-- misspelled or not a month name
    end
    
    -- returns true if date has one of the five seasons.  Else false.
    function is_valid_season (season)
    	if inArray( season, {'winter', 'spring', 'summer', 'fall', 'autumn'} ) then
    		return true;
    	end
    	return false;
    end
    
    --[[
    Returns true if day is less than or equal to the number of days in month; else returns false.
    
    Assumes Julian calendar prior to year 1582 and Gregorian calendar thereafter. Accounts for Julian calendar leap years before 1582 and Gregorian leap years after 1582.
    Where the two calendars overlap (1582 to approximately 1923) dates are assumed to be Gregorian.
    ]]
    function is_valid_date (year, month, day)
    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    local month_length;
    	if (2==month) then		-- if February
    		month_length = 28;	-- then 28 days unless
    		if 1582 > tonumber(year) then	-- Julian calendar
    			if 0==(year%4) then
    				month_length = 29;
    			end
    		else				-- Gregorian calendar
    			if (0==(year%4) and (0~=(year%100) or 0==(year%400))) then	-- date specifies a leap year
    				month_length = 29;	-- if leap year then 29 days in February
    			end
    		end
    	else
    		month_length=days_in_month[month];
    	end
    
    	if tonumber (day) > month_length then
    		return false;
    	end
    	return true;
    end
    
    --Check a pair of months or seasons to see if both are valid members of a month or season pair.
    
    function is_valid_month_season_range(range_start, range_end)
    	if 0 == get_month_number (range_start:lower()) then			-- is this a month range?
    		if true == is_valid_season (range_start:lower()) then	-- not a month range, is this a season range?
    			return is_valid_season (range_end:lower());			-- range_start is season; return true if range_end also a season; else false
    		end
    		return false;		-- range_start is not a month or a season
    	end
    	if 0 == get_month_number (range_end:lower()) then			-- range_start is a month; is range_end also a  month?
    		return false;											-- not a month range
    	end
    	return true;
    end
    
    
    --[[
    Check date format to see that it is one of the formats approved by MOS:DATE: MMMM D, YYYY; D MMMM YYYY; MMMM YYYY; YYYY-MM-DD; YYYY.
    Additionally, check the date to see that it is a real date: no 31 in 30-day months; no 29 February when not a leap year.  Months, both long-form and three
    character abbreviations, and seasons must be spelled correctly.
    
    If the date fails the fomat tests, this function returns false but does not return values for anchor_year and COinS_date.  When this happens, the date parameter is
    used in the COinS metadata and the CITEREF identifier gets its year from the year parameter if present.
    
    Inputs:
    	date_string - date string from date-holding parameters (date, year, accessdate, embargo, archivedate, etc)
    
    Returns:
    	false if date string is not a real date; else
    	true, anchor_year, COinS_date
    		anchor_year can be used in CITEREF anchors
    		COinS_date is date_string without anchor_year disambiguators if any
    ]]
    function check_date (date_string)
    	local year;
    	local month;
    	local day;
    	local anchor_year;
    	local coins_date;
    
    	if date_string:match("^%d%d%d%d%-%d%d%-%d%d$") then			-- Year-initial numerical year month day format
    		coins_date = date_string:match("%d%d%d%d%-%d%d%-%d%d");
    		year, month, day=string.match(date_string, "(%d%d%d%d)%-(%d%d)%-(%d%d)");
    		anchor_year = year;
    		month=tonumber(month);
    		if 12 < month or 1 > month then return false; end
    
    	elseif date_string:match("^%a+%s*%d%d*%s*,%s*%d%d%d%d%a?$") then	-- month-initial: month day, year
    		coins_date = date_string:match("%a+%s*%d%d*%s*,%s*%d%d%d%d");
    		month, day, anchor_year, year=string.match(date_string, "(%a+)%s*(%d%d*)%s*,%s*((%d%d%d%d)%a?)");
    		month = get_month_number (month:lower());
    		if 0 == month then return false; end					-- return false if month text isn't one of the twelve months
    				
    	elseif date_string:match("^%d%d*%s*%a+%s*%d%d%d%d%a?$") then		-- date-initial: day month year
    		coins_date = date_string:match("%d%d*%s*%a+%s*%d%d%d%d");
    		day, month, anchor_year, year=string.match(date_string, "(%d%d*)%s*(%a+)%s*((%d%d%d%d)%a?)");
    		month = get_month_number (month:lower());
    		if 0 == month then return false; end					-- return false if month text isn't one of the twelve months
    
    	elseif mw.ustring.match (date_string, "^%a+%s*[%s%-/–]%s*%a+%s*%d%d%d%d%a?$") then		-- month/season range year
    		local month2
    		coins_date = mw.ustring.match (date_string, "%a+%s*[%s%-/–]%s*%a+%s*%d%d%d%d");
    		coins_date= mw.ustring.gsub( coins_date, "–", "-" );	-- replace ndash with hyphen
    		month, month2, anchor_year, year=mw.ustring.match (date_string, "(%a+)%s*[%s%-/–]%s*(%a+)%s*((%d%d%d%d)%a?)");
    		day=0;													-- mark day as not used
    		if false == is_valid_month_season_range(month, month2) then
    			return false;
    		end
    		
    	elseif date_string:match("^%a+%s*%d%d%d%d%a?$") then				-- month/season year
    		coins_date = date_string:match("%a+%s*%d%d%d%d");
    		month, anchor_year, year=string.match(date_string, "(%a+)%s*((%d%d%d%d)%a?)");
    		day=0;													-- mark day as not used
    		local season=month;										-- copy 
    		month = get_month_number (month:lower());
    		if month == 0 then										-- if month text isn't one of the twelve months, might be a season
    			if false == is_valid_season (season:lower()) then
    				return false;									-- return false not a month or one of the five seasons
    			end
    		end
    
    	elseif date_string:match("^%d%d%d%d?%a?$") then			-- year; here accept either YYY or YYYY
    		coins_date = date_string:match("^%d%d%d%d?");
    		anchor_year, year=string.match(date_string, "((%d%d%d%d?)%a?)");
    		month, day = 0, 0;										-- mark day and month as not used
    	else
    		return false;											-- date format not one of the MOS:DATE approved formats
    	end
    
    	if 0~=month and 0~=day then									-- check year month day dates for validity
    		if false==is_valid_date(year,month,day) then
    			return false;										-- date string is not a real date return false; unset anchor_year and coins_date
    		end
    	end
    	
    	return true, anchor_year, coins_date;						-- format is good and date string represents a real date
    end	
    
    --[[
    Cycle the date-holding parameters in passed table date_parameters_list through check_date() to check compliance with MOS:DATE. For all valid dates, check_date() returns
    true and values for anchor_year (used in CITEREF identifiers) and COinS_date (used in the COinS metadata).  The |date= parameter test is unique.  This function only 
    accepts anchor_year and COinS_date results from the |date= parameter test and |date= is the only date-holding parameter that is allowed to contain the no-date keywords
    "n.d." or "nd" (without quotes).
    
    Unlike most error messages created in this module, only one error message is created by this function. Because all of the date holding parameters are processed serially,
    a single error message is created as the dates are tested.
    ]]
    
    function dates(date_parameters_list)
    	local anchor_year;		-- will return as nil if the date being tested is not |date=
    	local COinS_date;		-- will return as nil if the date being tested is not |date=
    	local error_message ="";
    	local good_date=false;
    	
    	for k, v in pairs(date_parameters_list) do		-- for each date-holding parameter in the list
    		if is_set(v) then							-- if the parameter has a value
    			if v:match("^c%.%s%d%d%d%d?%a?$") then		-- special case for c. year or with or without CITEREF disambiguator - only |date= and |year=
    				if 'date'==k then
    					good_date, anchor_year, COinS_date = true, v:match("((c%.%s%d%d%d%d?)%a?)");	-- anchor year and COinS_date only from |date= parameter
    				elseif 'year'==k then
    					good_date =  true;
    				end
    			elseif 'year'==k then					-- if the parameter is |year= (but not c. year)
    				if v:match("^%d%d%d%d?%a?$") then	-- year with or without CITEREF disambiguator
    					good_date =  true;
    				end
    			elseif 'date'==k then						-- if the parameter is |date=
    				if v:match("n%.d%.%a?") then		-- if |date=n.d. with or without a CITEREF disambiguator
    					good_date, anchor_year, COinS_date = true, v:match("((n%.d%.)%a?)");	--"n.d.";	-- no error when date parameter is set to no date
    				elseif v:match("nd%a?$") then		-- if |date=nd with or without a CITEREF disambiguator
    					good_date, anchor_year, COinS_date = true, v:match("((nd)%a?)");	--"nd";	-- no error when date parameter is set to no date
    				else
    					good_date, anchor_year, COinS_date = check_date (v);	-- go test the date
    				end
    			else									-- any other date-holding parameter
    				good_date = check_date (v);			-- go test the date
    			end
    			if false==good_date then		-- assemble one error message so we don't add the tracking category multiple times
    				if is_set(error_message) then		-- once we've added the first portion of the error message ...
    					error_message=error_message .. ", ";	-- ... add a comma space separator
    				end
    				error_message=error_message .. "&#124;" .. k .. "=";	-- add the failed parameter
    			end
    		end
    	end
    	if is_set(error_message) then
    		table.insert( z.message_tail, { seterror( 'bad_date', {error_message}, true ) } );	-- add this error message
    	end
    
    	return anchor_year, COinS_date;		-- and done
    end
    
    --[[
    Determines whether an URL string is valid
    
    At present the only check is whether the string appears to 
    be prefixed with a URI scheme.  It is not determined whether 
    the URI scheme is valid or whether the URL is otherwise well 
    formed.
    ]]
    function checkurl( url_str )
        -- Protocol-relative or URL scheme
        return url_str:sub(1,2) == "//" or url_str:match( "^[^/]*:" ) ~= nil;
    end
    
    -- Removes irrelevant text and dashes from ISBN number
    -- Similar to that used for Special:BookSources
    function cleanisbn( isbn_str )
        return isbn_str:gsub( "[^-0-9X]", "" );
    end
    
    --[[
    ISBN-10 and ISSN validator code calculates checksum across all isbn/issn digits including the check digit. If the number is valid the result will be 0.
    Before calling this function, issbn/issn must be checked for length and stripped of dashes, spaces and other non-isxn characters.
    ]]
    function is_valid_isxn (isxn_str, len)
    	local temp = 0;
    	isxn_str = { isxn_str:byte(1, len) };	-- make a table of bytes
    	len = len+1;							-- adjust to be a loop counter
    	for i, v in ipairs( isxn_str ) do				-- loop through all of the byte an calculate the checksum
    		if v == string.byte( "X" ) then		-- if checkdigit is X
    			temp = temp + 10*( len - i );	-- it represents 10 decimal
    		else
    			temp = temp + tonumber( string.char(v) )*(len-i);
    		end
    	end
    	return temp % 11 == 0;					-- returns true if calculation result is zero
    end
    
    -- Determines whether an ISBN string is valid
    function checkisbn( isbn_str )
        isbn_str = cleanisbn( isbn_str ):gsub( "-", "" );
        local len = isbn_str:len();
     
        if len ~= 10 and len ~= 13 then
            return false;
        end
     
        if len == 10 then
            if isbn_str:match( "^%d*X?$" ) == nil then return false; end
    		return is_valid_isxn(isbn_str, 10);
        else
    	    local temp = 0;
            if isbn_str:match( "^97[89]%d*$" ) == nil then return false; end	-- isbn13 begins with 978 or 979
            isbn_str = { isbn_str:byte(1, len) };
            for i, v in ipairs( isbn_str ) do
                temp = temp + (3 - 2*(i % 2)) * tonumber( string.char(v) );
            end
            return temp % 10 == 0;
        end
    end
    
    -- Gets the display text for a wikilink like [[A|B]] or [[B]] gives B
    function removewikilink( str )
        return (str:gsub( "%[%[([^%[%]]*)%]%]", function(l)
            return l:gsub( "^[^|]*|(.*)$", "%1" ):gsub("^%s*(.-)%s*$", "%1");
        end));
    end
    
    -- Escape sequences for content that will be used for URL descriptions
    function safeforurl( str )
        if str:match( "%[%[.-%]%]" ) ~= nil then 
            table.insert( z.message_tail, { seterror( 'wikilink_in_url', {}, true ) } );
        end
        
        return str:gsub( '[%[%]\n]', {    
            ['['] = '&#91;',
            [']'] = '&#93;',
            ['\n'] = ' ' } );
    end
    
    -- Converts a hyphen to a dash
    function hyphentodash( str )
        if not is_set(str) or str:match( "[%[%]{}<>]" ) ~= nil then
            return str;
        end    
        return str:gsub( '-', '–' );
    end
    
    -- Protects a string that will be wrapped in wiki italic markup '' ... ''
    function safeforitalics( str )
        --[[ Note: We can not use <i> for italics, as the expected behavior for
        italics specified by ''...'' in the title is that they will be inverted
        (i.e. unitalicized) in the resulting references.  In addition, <i> and ''
        tend to interact poorly under Mediawiki's HTML tidy. ]]
        
        if not is_set(str) then
            return str;
        else
            if str:sub(1,1) == "'" then str = "<span />" .. str; end
            if str:sub(-1,-1) == "'" then str = str .. "<span />"; end
            
            -- Remove newlines as they break italics.
            return str:gsub( '\n', ' ' );
        end
    end
    
    --[[
    Joins a sequence of strings together while checking for duplicate separation
    characters.
    ]]
    function safejoin( tbl, duplicate_char )
        --[[
        Note: we use string functions here, rather than ustring functions.
        
        This has considerably faster performance and should work correctly as 
        long as the duplicate_char is strict ASCII.  The strings
        in tbl may be ASCII or UTF8.
        ]]
        
        local str = '';
        local comp = '';
        local end_chr = '';
        local trim;
        for _, value in ipairs( tbl ) do
            if value == nil then value = ''; end
            
            if str == '' then
                str = value;
            elseif value ~= '' then
                if value:sub(1,1) == '<' then
                    -- Special case of values enclosed in spans and other markup.
                    comp = value:gsub( "%b<>", "" );
                else
                    comp = value;
                end
                
                if comp:sub(1,1) == duplicate_char then
                    trim = false;
                    end_chr = str:sub(-1,-1);
                    -- str = str .. "<HERE(enchr=" .. end_chr.. ")"
                    if end_chr == duplicate_char then
                        str = str:sub(1,-2);
                    elseif end_chr == "'" then
                        if str:sub(-3,-1) == duplicate_char .. "''" then
                            str = str:sub(1, -4) .. "''";
                        elseif str:sub(-5,-1) == duplicate_char .. "]]''" then
                            trim = true;
                        elseif str:sub(-4,-1) == duplicate_char .. "]''" then
                            trim = true;
                        end
                    elseif end_chr == "]" then
                        if str:sub(-3,-1) == duplicate_char .. "]]" then
                            trim = true;
                        elseif str:sub(-2,-1) == duplicate_char .. "]" then
                            trim = true;
                        end
                    elseif end_chr == " " then
                        if str:sub(-2,-1) == duplicate_char .. " " then
                            str = str:sub(1,-3);
                        end
                    end
    
                    if trim then
                        if value ~= comp then 
                            local dup2 = duplicate_char;
                            if dup2:match( "%A" ) then dup2 = "%" .. dup2; end
                            
                            value = value:gsub( "(%b<>)" .. dup2, "%1", 1 )
                        else
                            value = value:sub( 2, -1 );
                        end
                    end
                end
                str = str .. value;
            end
        end
        return str;
    end  
    
    --[[
    Return the year portion of a date string, if possible.  
    Returns empty string if the argument can not be interpreted
    as a year.
    
    BUG: If editors set |date=n.d or |date=n.da then selectyear() returns the current year for use in CITEREF.  These "dates" are caught by dates().
    ]]
    function selectyear( str )
    	-- Is the input a simple number?
    	local num = tonumber( str ); 
    	if num ~= nil and num > 0 and num < 2100 and num == math.floor(num) then
    		return str;
    	else
    		-- Use formatDate to interpret more complicated formats
    		local lang = mw.getContentLanguage();
    		local good, result;
    		good, result = pcall( lang.formatDate, lang, 'Y', str );
    		if good then return result; end -- if good
    	end
    end
    
    -- Attempts to convert names to initials.
    function reducetoinitials(first)
        local initials = {}
        for word in string.gmatch(first, "%S+") do
            table.insert(initials, string.sub(word,1,1)) -- Vancouver format does not include full stops.
        end
        return table.concat(initials) -- Vancouver format does not include spaces.
    end
    
    -- Formats a list of people (e.g. authors / editors) 
    function listpeople(control, people)
        local sep = control.sep;
        local namesep = control.namesep
        local format = control.format
        local maximum = control.maximum
        local lastauthoramp = control.lastauthoramp;
        local text = {}
        local etal = false;
        
        if sep:sub(-1,-1) ~= " " then sep = sep .. " " end
        if maximum ~= nil and maximum < 1 then return "", 0; end
        
        for i,person in ipairs(people) do
            if is_set(person.last) then
                local mask = person.mask
                local one
                local sep_one = sep;
                if maximum ~= nil and i > maximum then
                    etal = true;
                    break;
                elseif (mask ~= nil) then
                    local n = tonumber(mask)
                    if (n ~= nil) then
                        one = string.rep("&mdash;",n)
                    else
                        one = mask;
                        sep_one = " ";
                    end
                else
                    one = person.last
                    local first = person.first
                    if is_set(first) then 
                        if ( "vanc" == format ) then first = reducetoinitials(first) end
                        one = one .. namesep .. first 
                    end
                    if is_set(person.link) then one = "[[" .. person.link .. "|" .. one .. "]]" end
                end
                table.insert( text, one )
                table.insert( text, sep_one )
            end
        end
    
        local count = #text / 2;
        if count > 0 then 
            if count > 1 and is_set(lastauthoramp) and not etal then
                text[#text-2] = " & ";
            end
            text[#text] = nil; 
        end
        
        local result = table.concat(text) -- construct list
        if etal then 
            local etal_text = cfg.messages['et al'];
            result = result .. " " .. etal_text;
        end
        
        -- if necessary wrap result in <span> tag to format in Small Caps
        if ( "scap" == format ) then result = 
            '<span class="smallcaps" style="font-variant:small-caps">' .. result .. '</span>';
        end 
        return result, count
    end
    
    -- Generates a CITEREF anchor ID.
    function anchorid( options )
        return "CITEREF" .. table.concat( options );
    end
    
    -- Gets name list from the input arguments
    function extractnames(args, list_name)
        local names = {};
        local i = 1;
        local last;
        
        while true do
            last = selectone( args, cfg.aliases[list_name .. '-Last'], 'redundant_parameters', i );
            if not is_set(last) then
                -- just in case someone passed in an empty parameter
                break;
            end
            names[i] = {
                last = last,
                first = selectone( args, cfg.aliases[list_name .. '-First'], 'redundant_parameters', i ),
                link = selectone( args, cfg.aliases[list_name .. '-Link'], 'redundant_parameters', i ),
                mask = selectone( args, cfg.aliases[list_name .. '-Mask'], 'redundant_parameters', i )
            };
            i = i + 1;
        end
        return names;
    end
    
    -- Populates ID table from arguments using configuration settings
    function extractids( args )
        local id_list = {};
        for k, v in pairs( cfg.id_handlers ) do    
            v = selectone( args, v.parameters, 'redundant_parameters' );
            if is_set(v) then id_list[k] = v; end
        end
        return id_list;
    end
    
    -- Takes a table of IDs and turns it into a table of formatted ID outputs.
    function buildidlist( id_list, options )
        local new_list, handler = {};
        
        function fallback(k) return { __index = function(t,i) return cfg.id_handlers[k][i] end } end;
        
        for k, v in pairs( id_list ) do
            -- fallback to read-only cfg
            handler = setmetatable( { ['id'] = v }, fallback(k) );
            
            if handler.mode == 'external' then
                table.insert( new_list, {handler.label, externallinkid( handler ) } );
            elseif handler.mode == 'internal' then
                table.insert( new_list, {handler.label, internallinkid( handler ) } );
            elseif handler.mode ~= 'manual' then
                error( cfg.messages['unknown_ID_mode'] );
            elseif k == 'DOI' then
                table.insert( new_list, {handler.label, doi( v, options.DoiBroken ) } );
            elseif k == 'ASIN' then
                table.insert( new_list, {handler.label, amazon( v, options.ASINTLD ) } ); 
            elseif k == 'OL' then
                table.insert( new_list, {handler.label, openlibrary( v ) } );
            elseif k == 'PMC' then
                table.insert( new_list, {handler.label, pmc( v, options.Embargo ) } );
            elseif k == 'ISSN' then
            	table.insert( new_list, {handler.label, issn( v ) } );
            elseif k == 'ISBN' then
                local ISBN = internallinkid( handler );
                if not checkisbn( v ) and not is_set(options.IgnoreISBN) then
                    ISBN = ISBN .. seterror( 'bad_isbn', {}, false, " ", "" );
                end
                table.insert( new_list, {handler.label, ISBN } );                
            else
                error( cfg.messages['unknown_manual_ID'] );
            end
        end
        
        function comp( a, b )
            return a[1] < b[1];
        end
        
        table.sort( new_list, comp );
        for k, v in ipairs( new_list ) do
            new_list[k] = v[2];
        end
        
        return new_list;
    end
      
    -- Chooses one matching parameter from a list of parameters to consider
    -- Generates an error if more than one match is present.
    function selectone( args, possible, error_condition, index )
        local value = nil;
        local selected = '';
        local error_list = {};
        
        if index ~= nil then index = tostring(index); end
        
        -- Handle special case of "#" replaced by empty string
        if index == '1' then
            for _, v in ipairs( possible ) do
                v = v:gsub( "#", "" );
                if is_set(args[v]) then
                    if value ~= nil and selected ~= v then
                        table.insert( error_list, v );
                    else
                        value = args[v];
                        selected = v;
                    end
                end
            end        
        end
        
        for _, v in ipairs( possible ) do
            if index ~= nil then
                v = v:gsub( "#", index );
            end
            if is_set(args[v]) then
                if value ~= nil and selected ~=  v then
                    table.insert( error_list, v );
                else
                    value = args[v];
                    selected = v;
                end
            end
        end
        
        if #error_list > 0 then
            local error_str = "";
            for _, k in ipairs( error_list ) do
                if error_str ~= "" then error_str = error_str .. cfg.messages['parameter-separator'] end
                error_str = error_str .. wrap( 'parameter', k );
            end
            if #error_list > 1 then
                error_str = error_str .. cfg.messages['parameter-final-separator'];
            else
                error_str = error_str .. cfg.messages['parameter-pair-separator'];
            end
            error_str = error_str .. wrap( 'parameter', selected );
            table.insert( z.message_tail, { seterror( error_condition, {error_str}, true ) } );
        end
        
        return value, selected;
    end
    
    -- COinS metadata (see <http://ocoins.info/>) allows automated tools to parse
    -- the citation information.
    function COinS(data)
        if 'table' ~= type(data) or nil == next(data) then
            return '';
        end
        
        local ctx_ver = "Z39.88-2004";
        
        -- treat table strictly as an array with only set values.
        local OCinSoutput = setmetatable( {}, {
            __newindex = function(self, key, value)
                if is_set(value) then
                    rawset( self, #self+1, table.concat{ key, '=', mw.uri.encode( removewikilink( value ) ) } );
                end
            end
        });
        
        if is_set(data.Chapter) then
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
            OCinSoutput["rft.genre"] = "bookitem";
            OCinSoutput["rft.btitle"] = data.Chapter;
            OCinSoutput["rft.atitle"] = data.Title;
        elseif is_set(data.Periodical) then
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:journal";
            OCinSoutput["rft.genre"] = "article";
            OCinSoutput["rft.jtitle"] = data.Periodical;
            OCinSoutput["rft.atitle"] = data.Title;
        else
            OCinSoutput.rft_val_fmt = "info:ofi/fmt:kev:mtx:book";
            OCinSoutput["rft.genre"] = "book"
            OCinSoutput["rft.btitle"] = data.Title;
        end
        
        OCinSoutput["rft.place"] = data.PublicationPlace;
        OCinSoutput["rft.date"] = data.Date;
        OCinSoutput["rft.series"] = data.Series;
        OCinSoutput["rft.volume"] = data.Volume;
        OCinSoutput["rft.issue"] = data.Issue;
        OCinSoutput["rft.pages"] = data.Pages;
        OCinSoutput["rft.edition"] = data.Edition;
        OCinSoutput["rft.pub"] = data.PublisherName;
        
        for k, v in pairs( data.ID_list ) do
            local id, value = cfg.id_handlers[k].COinS;
            if k == 'ISBN' then value = cleanisbn( v ); else value = v; end
            if string.sub( id or "", 1, 4 ) == 'info' then
                OCinSoutput["rft_id"] = table.concat{ id, "/", v };
            else
                OCinSoutput[ id ] = value;
            end
        end
        
        local last, first;
        for k, v in ipairs( data.Authors ) do
            last, first = v.last, v.first;
            if k == 1 then
                if is_set(last) then
                    OCinSoutput["rft.aulast"] = last;
                end
                if is_set(first) then 
                    OCinSoutput["rft.aufirst"] = first;
                end
            end
            if is_set(last) and is_set(first) then
                OCinSoutput["rft.au"] = table.concat{ last, ", ", first };
            elseif is_set(last) then
                OCinSoutput["rft.au"] = last;
            end
        end
        
        OCinSoutput.rft_id = data.URL;
        OCinSoutput.rfr_id = table.concat{ "info:sid/", mw.site.server:match( "[^/]*$" ), ":", data.RawPage };
        OCinSoutput = setmetatable( OCinSoutput, nil );
        
        -- sort with version string always first, and combine.
        table.sort( OCinSoutput );
        table.insert( OCinSoutput, 1, "ctx_ver=" .. ctx_ver );  -- such as "Z39.88-2004"
        return table.concat(OCinSoutput, "&");
    end
    
    --[[
    This is the main function foing the majority of the citation
    formatting.
    ]]
    function citation0( config, args)
        --[[ 
        Load Input Parameters
        The argment_wrapper facillitates the mapping of multiple
        aliases to single internal variable.
        ]]
        local A = argument_wrapper( args );
    
        local i 
        local PPrefix = A['PPrefix']
        local PPPrefix = A['PPPrefix']
        if is_set( A['NoPP'] ) then PPPrefix = "" PPrefix = "" end
        
        -- Pick out the relevant fields from the arguments.  Different citation templates
        -- define different field names for the same underlying things.    
        local Authors = A['Authors'];
        local a = extractnames( args, 'AuthorList' );
    
        local Coauthors = A['Coauthors'];
        local Others = A['Others'];
        local Editors = A['Editors'];
        local e = extractnames( args, 'EditorList' );
    
        local Year = A['Year'];
        local PublicationDate = A['PublicationDate'];
        local OrigYear = A['OrigYear'];
        local Date = A['Date'];
        local LayDate = A['LayDate'];
        ------------------------------------------------- Get title data
        local Title = A['Title'];
        local BookTitle = A['BookTitle'];
        local Conference = A['Conference'];
        local TransTitle = A['TransTitle'];
        local TitleNote = A['TitleNote'];
        local TitleLink = A['TitleLink'];
        local Chapter = A['Chapter'];
        local ChapterLink = A['ChapterLink'];
        local TransChapter = A['TransChapter'];
        local TitleType = A['TitleType'];
        local Degree = A['Degree'];
        local Docket = A['Docket'];
        local ArchiveURL = A['ArchiveURL'];
        local URL = A['URL']
        local URLorigin = A:ORIGIN('URL');
        local ChapterURL = A['ChapterURL'];
        local ChapterURLorigin = A:ORIGIN('ChapterURL');
        local ConferenceURL = A['ConferenceURL'];
        local ConferenceURLorigin = A:ORIGIN('ConferenceURL');
        local Periodical = A['Periodical'];
        
        if ( config.CitationClass == "encyclopaedia" ) then
            if not is_set(Chapter) then
                if not is_set(Title) then
                    Title = Periodical;
                    Periodical = '';
                else
                    Chapter = Title
                    TransChapter = TransTitle
                    Title = '';
                    TransTitle = '';
                end
            end
        end
    
        local Series = A['Series'];
        local Volume = A['Volume'];
        local Issue = A['Issue'];
        local Position = '';
        local Page, Pages, At, page_type;
        
        Page = A['Page'];
        Pages = hyphentodash( A['Pages'] );
        At = A['At'];
        
        if is_set(Page) then
            if is_set(Pages) or is_set(At) then
                Page = Page .. " " .. seterror('extra_pages');
                Pages = '';
                At = '';
            end
        elseif is_set(Pages) then
            if is_set(At) then
                Pages = Pages .. " " .. seterror('extra_pages');
                At = '';
            end
        end    
        
        local Edition = A['Edition'];
        local PublicationPlace = A['PublicationPlace']
        local Place = A['Place'];
        
        if not is_set(PublicationPlace) and is_set(Place) then
            PublicationPlace = Place;
        end
        
        if PublicationPlace == Place then Place = ''; end
        
        local PublisherName = A['PublisherName'];
        local RegistrationRequired = A['RegistrationRequired'];
        local SubscriptionRequired = A['SubscriptionRequired'];
        local Via = A['Via'];
        local AccessDate = A['AccessDate'];
        local ArchiveDate = A['ArchiveDate'];
        local Agency = A['Agency'];
        local DeadURL = A['DeadURL']
        local Language = A['Language'];
        local Format = A['Format'];
        local Ref = A['Ref'];
        
        local DoiBroken = A['DoiBroken'];
    	
    -- Special case for cite techreport.
    	local ID = A['ID'];
    	if (config.CitationClass == "techreport") then	-- special case for cite techreport
    		if is_set(Issue) then						-- cite techreport uses 'number', which everything else aliases to 'issue'
    			if not is_set(ID) then					-- can we use ID for the "number"?
    				ID = Issue;							-- yes, use it
    				Issue = "";							-- unset Issue so that "number" isn't duplicated in the rendered citation or COinS metadata
    			else									-- can't use ID so emit error message
    				ID = ID .. " " .. seterror('redundant_parameters', '<code>&#124;id=</code> and <code>&#124;number=</code>');
    			end
    		end	
    	end	
    			
        local ASINTLD = A['ASINTLD'];
        local IgnoreISBN = A['IgnoreISBN'];
        local Embargo = A['Embargo'];
    
        local ID_list = extractids( args );
        
        local Quote = A['Quote'];
        local PostScript = A['PostScript'];
        local LayURL = A['LayURL'];
        local LaySource = A['LaySource'];
        local Transcript = A['Transcript'];
        local TranscriptURL = A['TranscriptURL'] 
        local TranscriptURLorigin = A:ORIGIN('TranscriptURL');
        local sepc = A['Separator'];
        local LastAuthorAmp = A['LastAuthorAmp'];
        local no_tracking_cats = A['NoTracking'];
    
        local use_lowercase = ( sepc ~= '.' );
        local this_page = mw.title.getCurrentTitle();  --Also used for COinS and for language
        
        if not is_set(no_tracking_cats) then
            for k, v in pairs( cfg.uncategorized_namespaces ) do
                if this_page.nsText == v then
                    no_tracking_cats = "true";
                    break;
                end
            end
        end
    
    	local anchor_year;		-- used in the CITEREF identifier
    	local COinS_date;		-- used in the COinS metadata
    
    -- Go test all of the date-holding parameters for valid MOS:DATE format and make sure that dates are real dates.
    -- TODO: 2013-10-27: AirDate is nil when dates() is called because it hasn't been set yet.  Move the call to dates() or set AirDate earlier.
    	anchor_year, COinS_date = dates({['accessdate']=AccessDate, ['airdate']=AirDate, ['archivedate']=ArchiveDate, ['date']=Date, ['doi_brokendate']=DoiBroken,
    		['embargo']=Embargo, ['laydate']=LayDate, ['publicationdate']=PublicationDate, ['year']=Year});
    
    	if not is_set(Year) then	-- prevent Year from being set from DateIn  TODO: eliminate the need for this?
    		if is_set(anchor_year) then
    			Year = anchor_year;
    		end
    	end
    
    -- At this point fields may be nil if they weren't specified in the template use.  We can use that fact.
        
        -- Account for the oddity that is {{cite conference}}, before generation of COinS data.
        if is_set(BookTitle) then
            Chapter = Title;
            ChapterLink = TitleLink;
            TransChapter = TransTitle;
            Title = BookTitle;
            TitleLink = '';
            TransTitle = '';
        end
        -- Account for the oddity that is {{cite episode}}, before generation of COinS data.
        if config.CitationClass == "episode" then
            local AirDate = A['AirDate'];
            local SeriesLink = A['SeriesLink'];
            local Season = A['Season'];
            local SeriesNumber = A['SeriesNumber'];
            local Network = A['Network'];
            local Station = A['Station'];
            local s, n = {}, {};
            local Sep = (first_set(A["SeriesSeparator"], A["Separator"]) or "") .. " ";
            
            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(Station) then table.insert(n, Station); end
            
            Date = Date or AirDate;
            Chapter = Title;
            ChapterLink = TitleLink;
            TransChapter = TransTitle;
            Title = Series;
            TitleLink = SeriesLink;
            TransTitle = '';
            
            Series = table.concat(s, Sep);
            ID = table.concat(n, Sep);
        end
        
        -- COinS metadata (see <http://ocoins.info/>) for
        -- automated parsing of citation information.
        local OCinSoutput = COinS{
            ['Periodical'] = Periodical,
            ['Chapter'] = Chapter,
            ['Title'] = Title,
            ['PublicationPlace'] = PublicationPlace,
            ['Date'] = first_set(COinS_date, Date, Year, PublicationDate),
            ['Series'] = Series,
            ['Volume'] = Volume,
            ['Issue'] = Issue,
            ['Pages'] = first_set(Page, Pages, At),
            ['Edition'] = Edition,
            ['PublisherName'] = PublisherName,
            ['URL'] = first_set( URL, ChapterURL ),
            ['Authors'] = a,
            ['ID_list'] = ID_list,
            ['RawPage'] = this_page.prefixedText,
        };
    
        if is_set(Periodical) and not is_set(Chapter) and is_set(Title) then
            Chapter = Title;
            ChapterLink = TitleLink;
            TransChapter = TransTitle;
            Title = '';
            TitleLink = '';
            TransTitle = '';
        end
    
        -- Now perform various field substitutions.
        -- We also add leading spaces and surrounding markup and punctuation to the
        -- various parts of the citation, but only when they are non-nil.
        if not is_set(Authors) then
            local Maximum = tonumber( A['DisplayAuthors'] );
            
            -- Preserve old-style implicit et al.
            if not is_set(Maximum) and #a == 9 then 
                Maximum = 8;
                table.insert( z.message_tail, { seterror('implict_etal_author', {}, true ) } );
            elseif not is_set(Maximum) then
                Maximum = #a + 1;
            end
                
            local control = { 
                sep = A["AuthorSeparator"] .. " ",
                namesep = (first_set(A["AuthorNameSeparator"], A["NameSeparator"]) or "") .. " ",
                format = A["AuthorFormat"],
                maximum = Maximum,
                lastauthoramp = LastAuthorAmp
            };
            
            -- If the coauthor field is also used, prevent ampersand and et al. formatting.
            if is_set(Coauthors) then
                control.lastauthoramp = nil;
                control.maximum = #a + 1;
                deprecated_parameter();		-- |coauthor= and |coathors= are deprecated; add this page to deprecated parameter category
            end
            
            Authors = listpeople(control, a) 
        end
    
    	if not is_set(Authors) and is_set(Coauthors) then	-- coauthors aren't displayed if one of authors=, authorn=, or lastn= isn't specified
    		table.insert( z.message_tail, { seterror('coauthors_missing_author', {}, true) } );	-- emit error message
    		deprecated_parameter();		-- |coauthor= and |coathors= are deprecated; add this page to deprecated parameter category
    	end
    
        local EditorCount
        if not is_set(Editors) then
            local Maximum = tonumber( A['DisplayEditors'] );
            -- Preserve old-style implicit et al.
            if not is_set(Maximum) and #e == 4 then 
                Maximum = 3;
                table.insert( z.message_tail, { seterror('implict_etal_editor', {}, true) } );
            elseif not is_set(Maximum) then
                Maximum = #e + 1;
            end
    
            local control = { 
                sep = A["EditorSeparator"] .. " ",
                namesep = (first_set(A["EditorNameSeparator"], A["NameSeparator"]) or "") .. " ",
                format = A['EditorFormat'],
                maximum = Maximum,
                lastauthoramp = LastAuthorAmp
            };
    
            Editors, EditorCount = listpeople(control, e);
        else
            EditorCount = 1;
        end
    
        local Cartography = "";
        local Scale = "";
        if config.CitationClass == "map" then
            if not is_set( Authors ) and is_set( PublisherName ) then
                Authors = PublisherName;
                PublisherName = "";
            end
            Cartography = A['Cartography'];
            if is_set( Cartography ) then
                Cartography = sepc .. " " .. wrap( 'cartography', Cartography, use_lowercase );
            end        
            Scale = A['Scale'];
            if is_set( Scale ) then
                Scale = sepc .. " " .. Scale;
            end        
        end
        
        if not is_set(Date) then
            Date = Year;
            if is_set(Date) then
                local Month = A['Month'];
                if is_set(Month) then
                	deprecated_parameter();		-- |month= (and also |day=) is deprecated; add this page to deprecated parameter category
                    Date = Month .. " " .. Date;
                    local Day = A['Day']
                    if is_set(Day) then Date = Day .. " " .. Date end
                end
            end
        end
        
        if inArray(PublicationDate, {Date, Year}) then PublicationDate = ''; end
        if not is_set(Date) and is_set(PublicationDate) then
            Date = PublicationDate;
            PublicationDate = '';
        end
    
        -- Captures the value for Date prior to adding parens or other textual transformations
        local DateIn = Date;
        
        if  not is_set(URL) and
            not is_set(ChapterURL) and
            not is_set(ArchiveURL) and
            not is_set(ConferenceURL) and
            not is_set(TranscriptURL) then
            
            -- Test if cite web is called without giving a URL
            if ( config.CitationClass == "web" ) then
                table.insert( z.message_tail, { seterror( 'cite_web_url', {}, true ) } );
            end
            
            -- Test if accessdate is given without giving a URL
            if is_set(AccessDate) then
                table.insert( z.message_tail, { seterror( 'accessdate_missing_url', {}, true ) } );
                AccessDate = '';
            end
            
            -- Test if format is given without giving a URL
            if is_set(Format) then
                Format = Format .. seterror( 'format_missing_url' );
            end
        end
        
        -- Test if citation has no title
        if  not is_set(Chapter) and
            not is_set(Title) and
            not is_set(Periodical) and
            not is_set(Conference) and
            not is_set(TransTitle) and
            not is_set(TransChapter) then
            table.insert( z.message_tail, { seterror( 'citation_missing_title', {}, true ) } );
        end
        
        Format = is_set(Format) and " (" .. Format .. ")" or "";
        
        local OriginalURL = URL
        DeadURL = DeadURL:lower();
        if is_set( ArchiveURL ) then
            if ( DeadURL ~= "no" ) then
                URL = ArchiveURL
                URLorigin = A:ORIGIN('ArchiveURL')
            end
        end
        
        -- Format chapter / article title
        if is_set(Chapter) and is_set(ChapterLink) then 
            Chapter = "[[" .. ChapterLink .. "|" .. Chapter .. "]]";
        end
        if is_set(Periodical) and is_set(Title) then
            Chapter = wrap( 'italic-title', Chapter );
            TransChapter = wrap( 'trans-italic-title', TransChapter );
        else
            Chapter = wrap( 'quoted-title', Chapter );
            TransChapter = wrap( 'trans-quoted-title', TransChapter );
        end
        
        local TransError = ""
        if is_set(TransChapter) then
            if not is_set(Chapter) then
                TransError = " " .. seterror( 'trans_missing_chapter' );
            else
                TransChapter = " " .. TransChapter;
            end
        end
        
        Chapter = Chapter .. TransChapter;
        
        if is_set(Chapter) then
            if not is_set(ChapterLink) then
                if is_set(ChapterURL) then
                    Chapter = externallink( ChapterURL, Chapter ) .. TransError;
                    if not is_set(URL) then
                        Chapter = Chapter .. Format;
                        Format = "";
                    end
                elseif is_set(URL) then 
                    Chapter = externallink( URL, Chapter ) .. TransError .. Format;
                    URL = "";
                    Format = "";
                else
                    Chapter = Chapter .. TransError;
                end            
            elseif is_set(ChapterURL) then
                Chapter = Chapter .. " " .. externallink( ChapterURL, nil, ChapterURLorigin ) .. 
                    TransError;
            else
                Chapter = Chapter .. TransError;
            end
            Chapter = Chapter .. sepc .. " " -- with end-space
        elseif is_set(ChapterURL) then
            Chapter = " " .. externallink( ChapterURL, nil, ChapterURLorigin ) .. sepc .. " ";
        end        
        
        -- Format main title.
        if is_set(TitleLink) and is_set(Title) then
            Title = "[[" .. TitleLink .. "|" .. Title .. "]]"
        end
        
        if is_set(Periodical) then
            Title = wrap( 'quoted-title', Title );
            TransTitle = wrap( 'trans-quoted-title', TransTitle );
        elseif inArray(config.CitationClass, {"web","news","pressrelease","conference"}) and
                not is_set(Chapter) then
            Title = wrap( 'quoted-title', Title );
            TransTitle = wrap( 'trans-quoted-title', TransTitle );
        else
            Title = wrap( 'italic-title', Title );
            TransTitle = wrap( 'trans-italic-title', TransTitle );
        end
        
        TransError = "";
        if is_set(TransTitle) then
            if not is_set(Title) then
                TransError = " " .. seterror( 'trans_missing_title' );
            else
                TransTitle = " " .. TransTitle;
            end
        end
        
        Title = Title .. TransTitle;
        
        if is_set(Title) then
            if not is_set(TitleLink) and is_set(URL) then 
                Title = externallink( URL, Title ) .. TransError .. Format       
                URL = "";
                Format = "";
            else
                Title = Title .. TransError;
            end
        end
        
        if is_set(Place) then
            Place = " " .. wrap( 'written', Place, use_lowercase ) .. sepc .. " ";
        end
        
        if is_set(Conference) then
            if is_set(ConferenceURL) then
                Conference = externallink( ConferenceURL, Conference );
            end
            Conference = sepc .. " " .. Conference
        elseif is_set(ConferenceURL) then
            Conference = sepc .. " " .. externallink( ConferenceURL, nil, ConferenceURLorigin );
        end
        
        if not is_set(Position) then
            local Minutes = A['Minutes'];
            if is_set(Minutes) then
                Position = " " .. Minutes .. " " .. cfg.messages['minutes'];
            else
                local Time = A['Time'];
                if is_set(Time) then
                    local TimeCaption = A['TimeCaption']
                    if not is_set(TimeCaption) then
                        TimeCaption = cfg.messages['event'];
                        if sepc ~= '.' then
                            TimeCaption = TimeCaption:lower();
                        end
                    end
                    Position = " " .. TimeCaption .. " " .. Time;
                end
            end
        else
            Position = " " .. Position;
            At = '';
        end
        
        if not is_set(Page) then
            if is_set(Pages) then
                if is_set(Periodical) and
                    not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
                    Pages = ": " .. Pages;
                elseif tonumber(Pages) ~= nil then
                    Pages = sepc .." " .. PPrefix .. Pages;
                else
                    Pages = sepc .." " .. PPPrefix .. Pages;
                end
            end
        else
            if is_set(Periodical) and
                not inArray(config.CitationClass, {"encyclopaedia","web","book","news"}) then
                Page = ": " .. Page;
            else
                Page = sepc .." " .. PPrefix .. Page;
            end
        end
        
        At = is_set(At) and (sepc .. " " .. At) or "";
        Position = is_set(Position) and (sepc .. " " .. Position) or "";
        if config.CitationClass == 'map' then
            local Section = A['Section'];
            local Inset = A['Inset'];
            if first_set( Pages, Page, At ) ~= nil or sepc ~= '.' then
                if is_set( Section ) then
                    Section = ", " .. wrap( 'section', Section, true );
                end
                if is_set( Inset ) then
                    Inset = ", " .. wrap( 'inset', Inset, true );
                end
            else
                if is_set( Section ) then
                    Section = sepc .. " " .. wrap( 'section', Section, use_lowercase );
                    if is_set( Inset ) then
                        Inset = ", " .. wrap( 'inset', Inset, true );
                    end
                elseif is_set( Inset ) then
                    Inset = sepc .. " " .. wrap( 'inset', Inset, use_lowercase );
                end            
            end            
            At = At .. Section .. Inset;        
        end    
    
    	--[[Look in the list of iso639-1 language codes to see if the value provided in the language parameter matches one of them.  If a match is found, 
    	use that value; if not, then use the value that was provided with the language parameter.
    	
    	Categories are assigned in a manner similar to the {{xx icon}} templates - categorizes only mainspace citations and only when the language code is not 'en' (English).
    	]]
    	if is_set (Language) then
    		local name = cfg.iso639_1[Language:lower()];		-- get the language name if Language parameter has a valid iso 639-1 code
    		if nil == name then
    			Language=" " .. wrap( 'language', Language );	-- no match, use parameter's value
    		else
    			if 0 == this_page.namespace and 'en' ~= Language:lower() then	--found a match; is this page main / article space and English not the language?
    				Language=" " .. wrap( 'language', name .. '[[Category:Articles with ' .. name .. '-language external links]]' );	-- in main space and not English: categorize
    			else
    				Language=" " .. wrap( 'language', name );	--not in mainspace or language is English so don't categorize
    			end
    		end
    	else
    		Language="";	-- language not specified so make sure this is an empty string;
    	end
    
    	Others = is_set(Others) and (sepc .. " " .. Others) or "";
    
    -- handle type parameter for those CS1 citations that have default values
    
    	if "pressrelease" == config.CitationClass then		-- if this citation is cite press release
    		if not is_set (TitleType) then
    			TitleType = "Press release";				-- if type not specified, display the press release annotation
    		else
    			if "none" == TitleType then TitleType = ""; end		-- if |type=none then type parameter not displayed
    		end
    
    	elseif "techreport" == config.CitationClass then	-- if this citation is cite techreport
    		if not is_set (TitleType) then
    			TitleType = "Technical report";				-- if type not specified, display the techreport annotation
    		else
    			if "none" == TitleType then TitleType = ""; end		-- if |type=none then type parameter not displayed; |number=, if set, will be displayed
    		end
    	
    	elseif "thesis" == config.CitationClass then		-- if this citation is cite thesis
    		if not is_set (TitleType) then
    			if is_set(Degree) then						-- if type not specified, display one of the thesis annotations
    				TitleType = Degree .. " thesis";		-- if a degree (masters, PhD, ...) is specified include it in the display
    			else
    				TitleType = "Thesis";					-- otherwise display the simple thesis annotation
    			end
    		else
    			if "none" == TitleType then TitleType = ""; end		-- if |type=none then type parameter not displayed
    		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 "";
        Edition = is_set(Edition) and (" " .. wrap( 'edition', Edition )) or "";
        Issue = is_set(Issue) and (" (" .. Issue .. ")") or "";
        Series = is_set(Series) and (sepc .. " " .. Series) or "";
        OrigYear = is_set(OrigYear) and (" [" .. OrigYear .. "]") or "";
        Agency = is_set(Agency) and (sepc .. " " .. Agency) or "";
        
        if is_set(Volume) then
            if ( mw.ustring.len(Volume) > 4 )
              then Volume = sepc .." " .. Volume;
              else Volume = " <b>" .. hyphentodash(Volume) .. "</b>";
            end
        end
        
        ------------------------------------ totally unrelated data
        --[[ Loosely mimic {{subscription required}} template; Via parameter identifies a delivery source that is not the publisher; these sources often, but not always, exist
        behind a registration or paywall.  So here, we've chosen to decouple via from subscription (via has never been part of the registration required template).
        
        Subscription implies paywall; Registration does not.  If both are used in a citation, the subscription required link note is displayed. There are no error messages for this condition.
        ]]
        if is_set(Via) then
            Via = " " .. wrap( 'via', Via );
        end
    
    	if is_set(SubscriptionRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['subscription']; --here when 'via' parameter not used but 'subscription' is
        elseif is_set(RegistrationRequired) then
            SubscriptionRequired = sepc .. " " .. cfg.messages['registration']; --here when 'via' and 'subscription' parameters not used but 'registration' is
        end
    
        if is_set(AccessDate) then
            local retrv_text = " " .. cfg.messages['retrieved']
            if (sepc ~= ".") then retrv_text = retrv_text:lower() end
            AccessDate = '<span class="reference-accessdate">' .. sepc
                .. substitute( retrv_text, {AccessDate} ) .. '</span>'
        end
        
        if is_set(ID) then ID = sepc .." ".. ID; end
       	if "thesis" == config.CitationClass and is_set(Docket) then
    		ID = sepc .." Docket ".. Docket .. ID;
    	end
    
        
        ID_list = buildidlist( ID_list, {DoiBroken = DoiBroken, ASINTLD = ASINTLD, IgnoreISBN = IgnoreISBN, Embargo=Embargo} );
    
        if is_set(URL) then
            URL = " " .. externallink( URL, nil, URLorigin );
        end
    
        if is_set(Quote) then
            if Quote:sub(1,1) == '"' and Quote:sub(-1,-1) == '"' then
                Quote = Quote:sub(2,-2);
            end
            Quote = sepc .." " .. wrap( 'quoted-text', Quote ); 
            PostScript = "";
        elseif PostScript:lower() == "none" then
            PostScript = "";
        end
        
        local Archived
        if is_set(ArchiveURL) then
            if not is_set(ArchiveDate) then
                ArchiveDate = seterror('archive_missing_date');
            end
            if "no" == DeadURL then
                local arch_text = cfg.messages['archived'];
                if sepc ~= "." then arch_text = arch_text:lower() end
                Archived = sepc .. " " .. substitute( cfg.messages['archived-not-dead'],
                    { externallink( ArchiveURL, arch_text ), ArchiveDate } );
                if not is_set(OriginalURL) then
                    Archived = Archived .. " " .. seterror('archive_missing_url');                               
                end
            elseif is_set(OriginalURL) then
                local arch_text = cfg.messages['archived-dead'];
                if sepc ~= "." then arch_text = arch_text:lower() end
                Archived = sepc .. " " .. substitute( arch_text,
                    { externallink( OriginalURL, cfg.messages['original'] ), ArchiveDate } );
            else
                local arch_text = cfg.messages['archived-missing'];
                if sepc ~= "." then arch_text = arch_text:lower() end
                Archived = sepc .. " " .. substitute( arch_text, 
                    { seterror('archive_missing_url'), ArchiveDate } );
            end
        else
            Archived = ""
        end
        
        local Lay
        if is_set(LayURL) then
            if is_set(LayDate) then LayDate = " (" .. LayDate .. ")" end
            if is_set(LaySource) then 
                LaySource = " &ndash; ''" .. safeforitalics(LaySource) .. "''";
            else
                LaySource = "";
            end
            if sepc == '.' then
                Lay = sepc .. " " .. externallink( LayURL, cfg.messages['lay summary'] ) .. LaySource .. LayDate
            else
                Lay = sepc .. " " .. externallink( LayURL, cfg.messages['lay summary']:lower() ) .. LaySource .. LayDate
            end            
        else
            Lay = "";
        end
        
        if is_set(Transcript) then
            if is_set(TranscriptURL) then Transcript = externallink( TranscriptURL, Transcript ); end
        elseif is_set(TranscriptURL) then
            Transcript = externallink( TranscriptURL, nil, TranscriptURLorigin );
        end
        
        local Publisher;
        if is_set(Periodical) and
            not inArray(config.CitationClass, {"encyclopaedia","web","pressrelease"}) then
            if is_set(PublisherName) then
                if is_set(PublicationPlace) then
                    Publisher = PublicationPlace .. ": " .. PublisherName;
                else
                    Publisher = PublisherName;  
                end
            elseif is_set(PublicationPlace) then
                Publisher= PublicationPlace;
            else 
                Publisher = "";
            end
            if is_set(PublicationDate) then
                if is_set(Publisher) then
                    Publisher = Publisher .. ", " .. wrap( 'published', PublicationDate );
                else
                    Publisher = PublicationDate;
                end
            end
            if is_set(Publisher) then
                Publisher = " (" .. Publisher .. ")";
            end
        else
            if is_set(PublicationDate) then
                PublicationDate = " (" .. wrap( 'published', PublicationDate ) .. ")";
            end
            if is_set(PublisherName) then
                if is_set(PublicationPlace) then
                    Publisher = sepc .. " " .. PublicationPlace .. ": " .. PublisherName .. PublicationDate;
                else
                    Publisher = sepc .. " " .. PublisherName .. PublicationDate;  
                end            
            elseif is_set(PublicationPlace) then 
                Publisher= sepc .. " " .. PublicationPlace .. PublicationDate;
            else 
                Publisher = PublicationDate;
            end
        end
        
        -- Several of the above rely upon detecting this as nil, so do it last.
        if is_set(Periodical) then
            if is_set(Title) or is_set(TitleNote) then 
                Periodical = sepc .. " " .. wrap( 'italic-title', Periodical ) 
            else 
                Periodical = wrap( 'italic-title', Periodical )
            end
        end
    
        -- Piece all bits together at last.  Here, all should be non-nil.
        -- We build things this way because it is more efficient in LUA
        -- not to keep reassigning to the same string variable over and over.
    
        local tcommon
        if inArray(config.CitationClass, {"journal","citation"}) and is_set(Periodical) then
            if is_set(Others) then Others = Others .. sepc .. " " end
            tcommon = safejoin( {Others, Title, TitleNote, Conference, Periodical, Format, TitleType, Scale, Series, 
                Language, Cartography, Edition, Publisher, Agency, Volume, Issue}, sepc );
        else 
            tcommon = safejoin( {Title, TitleNote, Conference, Periodical, Format, TitleType, Scale, Series, Language, 
                Volume, Issue, Others, Cartography, Edition, Publisher, Agency}, sepc );
        end
        
        if #ID_list > 0 then
            ID_list = safejoin( { sepc .. " ",  table.concat( ID_list, sepc .. " " ), ID }, sepc );
        else
            ID_list = ID;
        end
        
        local idcommon = safejoin( { ID_list, URL, Archived, AccessDate, Via, SubscriptionRequired, Lay, Quote }, sepc );
        local text;
        local pgtext = Position .. Page .. Pages .. At;
        
        if is_set(Authors) then
            if is_set(Coauthors) then
                Authors = Authors .. A['AuthorSeparator'] .. " " .. Coauthors
            end
            if is_set(Date) then
                Date = " ("..Date..")" .. OrigYear .. sepc .. " "
            elseif string.sub(Authors,-1,-1) == sepc then
                Authors = Authors .. " "
            else
                Authors = Authors .. sepc .. " "
            end
            if is_set(Editors) then
                local in_text = " ";
                local post_text = "";
                if is_set(Chapter) then
                    in_text = in_text .. cfg.messages['in'] .. " "
                else
                    if EditorCount <= 1 then
                        post_text = ", " .. cfg.messages['editor'];
                    else
                        post_text = ", " .. cfg.messages['editors'];
                    end
                end 
                if (sepc ~= '.') then in_text = in_text:lower() end
                Editors = in_text .. Editors .. post_text;
                if (string.sub(Editors,-1,-1) == sepc)
                    then Editors = Editors .. " "
                    else Editors = Editors .. sepc .. " "
                end
            end
            text = safejoin( {Authors, Date, Chapter, Place, Editors, tcommon }, sepc );
            text = safejoin( {text, pgtext, idcommon}, sepc );
        elseif is_set(Editors) then
            if is_set(Date) then
                if EditorCount <= 1 then
                    Editors = Editors .. ", " .. cfg.messages['editor'];
                else
                    Editors = Editors .. ", " .. cfg.messages['editors'];
                end
                Date = " (" .. Date ..")" .. OrigYear .. sepc .. " "
            else
                if EditorCount <= 1 then
                    Editors = Editors .. " (" .. cfg.messages['editor'] .. ")" .. sepc .. " "
                else
                    Editors = Editors .. " (" .. cfg.messages['editors'] .. ")" .. sepc .. " "
                end
            end
            text = safejoin( {Editors, Date, Chapter, Place, tcommon}, sepc );
            text = safejoin( {text, pgtext, idcommon}, sepc );
        else
            if is_set(Date) then
                if ( string.sub(tcommon,-1,-1) ~= sepc )
                  then Date = sepc .." " .. Date .. OrigYear
                  else Date = " " .. Date .. OrigYear
                end
            end
            if config.CitationClass=="journal" and is_set(Periodical) then
                text = safejoin( {Chapter, Place, tcommon}, sepc );
                text = safejoin( {text, pgtext, Date, idcommon}, sepc );
            else
                text = safejoin( {Chapter, Place, tcommon, Date}, sepc );
                text = safejoin( {text, pgtext, idcommon}, sepc );
            end
        end
        
        if is_set(PostScript) and PostScript ~= sepc then
            text = safejoin( {text, sepc}, sepc );  --Deals with italics, spaces, etc.
            text = text:sub(1,-2); --Remove final seperator    
        end    
        
        text = safejoin( {text, PostScript}, sepc );
    
        -- Now enclose the whole thing in a <span/> element
        if not is_set(Year) then
            if is_set(DateIn) then
                Year = selectyear( DateIn );
            elseif is_set(PublicationDate) then
                Year = selectyear( PublicationDate );
            end
        end
        
        local options = {};
        
        if is_set(config.CitationClass) and config.CitationClass ~= "citation" then
            options.class = "citation " .. config.CitationClass;
        else
            options.class = "citation";
        end
        
        if is_set(Ref) and Ref:lower() ~= "none" then
            local id = Ref
            if ( "harv" == Ref ) then
                local names = {} --table of last names & year
                if #a > 0 then
                    for i,v in ipairs(a) do 
                        names[i] = v.last 
                        if i == 4 then break end
                    end
                elseif #e > 0 then
                    for i,v in ipairs(e) do 
                        names[i] = v.last 
                        if i == 4 then break end                
                    end
                end
    			names[ #names + 1 ] = Year or anchor_year;	-- Year first for legacy citations
                id = anchorid(names)
            end
            options.id = id;
        end
        
        if string.len(text:gsub("<span[^>/]*>.-</span>", ""):gsub("%b<>","")) <= 2 then
            z.error_categories = {};
            text = seterror('empty_citation');
            z.message_tail = {};
        end
        
        if is_set(options.id) then 
            text = '<span id="' .. mw.uri.anchorEncode(options.id) ..'" class="' .. mw.text.nowiki(options.class) .. '">' .. text .. "</span>";
        else
            text = '<span class="' .. mw.text.nowiki(options.class) .. '">' .. text .. "</span>";
        end        
    
        local empty_span = '<span style="display:none;">&nbsp;</span>';
        
        -- Note: Using display: none on then COinS span breaks some clients.
        local OCinS = '<span title="' .. OCinSoutput .. '" class="Z3988">' .. empty_span .. '</span>';
        text = text .. OCinS;
        
        if #z.message_tail ~= 0 then
            text = text .. " ";
            for i,v in ipairs( z.message_tail ) do
                if is_set(v[1]) then
                    if i == #z.message_tail then
                        text = text .. errorcomment( v[1], v[2] );
                    else
                        text = text .. errorcomment( v[1] .. "; ", v[2] );
                    end
                end
            end
        end
        
        no_tracking_cats = no_tracking_cats:lower();
        if inArray(no_tracking_cats, {"", "no", "false", "n"}) then
            for _, v in ipairs( z.error_categories ) do
                text = text .. '[[Category:' .. v ..']]';
            end
        end
        
        return text
    end
    
    -- This is used by templates such as {{cite book}} to create the actual citation text.
    function z.citation(frame)
        local pframe = frame:getParent()
        
        local args = {};
        local suggestions = {};
        local error_text, error_state;
    
        local config = {};
        for k, v in pairs( frame.args ) do
            config[k] = v;
            args[k] = v;       
        end    
    
        for k, v in pairs( pframe.args ) do
            if v ~= '' then
                if not validate( k ) then            
                    error_text = "";
                    if type( k ) ~= 'string' then
                        -- Exclude empty numbered parameters
                        if v:match("%S+") ~= nil then
                            error_text, error_state = seterror( 'text_ignored', {v}, true );
                        end
                    elseif validate( k:lower() ) then 
                        error_text, error_state = seterror( 'parameter_ignored_suggest', {k, k:lower()}, true );
                    else
                        if #suggestions == 0 then
                            suggestions = mw.loadData( 'Module:Citation/CS1/Suggestions' );
                        end
                        if suggestions[ k:lower() ] ~= nil then
                            error_text, error_state = seterror( 'parameter_ignored_suggest', {k, suggestions[ k:lower() ]}, true );
                        else
                            error_text, error_state = seterror( 'parameter_ignored', {k}, true );
                        end
                    end                  
                    if error_text ~= '' then
                        table.insert( z.message_tail, {error_text, error_state} );
                    end                
                end
                args[k] = v;
            elseif args[k] ~= nil or (k == 'postscript') then
                args[k] = v;
            end        
        end    
        
        return citation0( config, args)
    end
    
    return z