Difference between revisions of "Module:Webarchive"

From Sim Football Wiki
Jump to navigation Jump to search
m (1 revision imported)
 
m (1 revision imported)
 
(One intermediate revision by one other user not shown)
Line 1: Line 1:
 
--[[ ----------------------------------
 
--[[ ----------------------------------
  
    Lua module implementing the {{webarchive}} template.  
+
Lua module implementing the {{webarchive}} template.  
  
      A merger of the functionality of three templates: {{wayback}}, {{webcite}} and {{cite archives}}
+
A merger of the functionality of three templates: {{wayback}}, {{webcite}} and {{cite archives}}
 
+
  ]]
+
]]
  
local p = {}
 
  
--[[--------------------------< inlineError >-----------------------
+
--[[--------------------------< D E P E N D E N C I E S >------------------------------------------------------
 +
]]
 +
 
 +
require('Module:No globals');
 +
local getArgs = require ('Module:Arguments').getArgs;
 +
 
 +
 
 +
--[[--------------------------< F O R W A R D  D E C L A R A T I O N S >--------------------------------------
 +
]]
 +
 
 +
local categories = {}; -- category names
 +
local config = {}; -- global configuration settings
 +
local digits = {}; -- for i18n; table that translates local-wiki digits to western digits
 +
local err_warn_msgs = {}; -- error and warning messages
 +
local excepted_pages = {};
 +
local month_num = {}; -- for i18n; table that translates local-wiki month names to western digits
 +
local prefixes = {}; -- service provider tail string prefixes
 +
local services = {}; -- archive service provider data from
 +
local s_text = {}; -- table of static text strings used to build final rendering
 +
local uncategorized_namespaces = {}; -- list of namespaces that we should not categorize
 +
local uncategorized_subpages = {}; -- list of subpages that should not be categorized
 +
 
 +
 
 +
--[[--------------------------< P A G E  S C O P E  I D E N T I F I E R S >----------------------------------
 +
]]
 +
 
 +
local non_western_digits; -- boolean flag set true when data.digits.enable is true
 +
local this_page = mw.title.getCurrentTitle();
 +
 
 +
local track = {}; -- Associative array to hold tracking categories
 +
local ulx = {}; -- Associative array to hold template data
 +
 
 +
 
 +
--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------
 +
 
 +
Populates numbered arguments in a message string using an argument table.
 +
 
 +
]]
 +
 
 +
local function substitute (msg, args)
 +
return args and mw.message.newRawMessage (msg, args):plain() or msg;
 +
end
 +
 
 +
 
 +
--[[--------------------------< tableLength >-----------------------
 +
 
 +
Given a 1-D table, return number of elements
 +
 
 +
]]
 +
 
 +
local function tableLength(T)
 +
local count = 0
 +
for _ in pairs(T) do count = count + 1 end
 +
return count
 +
end
 +
 
  
    Critical error. Render output completely in red. Add to tracking category.
+
--[=[-------------------------< M A K E _ W I K I L I N K >----------------------------------------------------
  
]]
+
Makes a wikilink; when both link and display text is provided, returns a wikilink in the form [[L|D]]; if only
 +
link is provided, returns a wikilink in the form [[L]]; if neither are provided or link is omitted, returns an
 +
empty string.
  
local function inlineError(arg, msg)
+
]=]
  
  track["Category:Webarchive template errors"] = 1
+
local function make_wikilink (link, display, no_link)
  return '<span style="font-size:100%" class="error citation-comment">Error in webarchive template: Check <code style="color:inherit; border:inherit; padding:inherit;">&#124;' .. arg .. '=</code> value. ' .. msg .. '</span>'
+
if nil == no_link then
 +
if link and ('' ~= link) then
 +
if display and ('' ~= display) then
 +
return table.concat ({'[[', link, '|', display, ']]'});
 +
else
 +
return table.concat ({'[[', link, ']]'});
 +
end
 +
end
 +
return display or ''; -- link not set so return the display text
  
 +
else -- no_link
 +
if display and ('' ~= display) then -- if there is display text
 +
return display; -- return that
 +
else
 +
return link or ''; -- return the target article name or empty string
 +
end
 +
end
 
end
 
end
  
--[[--------------------------< inlineRed >-----------------------
 
  
      Render a text fragment in red, such as a warning as part of the final output.
+
--[[--------------------------< createTracking >-----------------------
      Add tracking category.
+
 
 +
Return data in track[] ie. tracking categories
 +
 
 +
]]
 +
 
 +
local function createTracking()
 +
if not excepted_pages[this_page.fullText] then -- namespace:title/fragment is allowed to be categorized (typically this module's / template's testcases page(s))
 +
if uncategorized_namespaces[this_page.nsText] then
 +
return ''; -- this page not to be categorized so return empty string
 +
end
 +
for _,v in ipairs (uncategorized_subpages) do -- cycle through page name patterns
 +
if this_page.text:match (v) then -- test page name against each pattern
 +
return ''; -- this subpage type not to be categorized so return empty string
 +
end
 +
end
 +
end
 +
 
 +
local out = {};
 +
if tableLength(track) > 0 then
 +
for key, _ in pairs(track) do -- loop through table
 +
table.insert (out, make_wikilink (key)); -- and convert category names to links
 +
end
 +
end
 +
return table.concat (out); -- concat into one big string; empty string if table is empty
 +
 
 +
end
 +
 
  
]]
+
--[[--------------------------< inlineError >-----------------------
  
local function inlineRed(msg, trackmsg)
+
Critical error. Render output completely in red. Add to tracking category.
  
  if trackmsg == "warning" then
+
This function called as the last thing before abandoning this module
    track["Category:Webarchive template warnings"] = 1
 
  elseif trackmsg == "error" then
 
    track["Category:Webarchive template errors"] = 1
 
  end
 
  
  return '<span style="font-size:100%" class="error citation-comment">' .. msg .. '</span>'
+
]]
  
 +
local function inlineError (msg, args)
 +
track[categories.error] = 1
 +
return table.concat ({
 +
'<span style="font-size:100%" class="error citation-comment">Error in ', -- open the error message span
 +
config.tname, -- insert the local language template name
 +
' template: ',
 +
substitute (msg, args), -- insert the formatted error message
 +
'.</span>', -- close the span
 +
createTracking() -- add the category
 +
})
 
end
 
end
  
--[[--------------------------< trimArg >-----------------------
 
  
    trimArg returns nil if arg is "" while trimArg2 returns 'true' if arg is ""
+
--[[--------------------------< inlineRed >-----------------------
    trimArg2 is for args that might accept an empty value, as an on/off switch like nolink=
+
 
 +
Render a text fragment in red, such as a warning as part of the final output.
 +
Add tracking category.
  
 
  ]]
 
  ]]
  
local function trimArg(arg)
+
local function inlineRed(msg, trackmsg)
  if arg == "" or arg == nil then
+
if trackmsg == "warning" then
    return nil
+
track[categories.warning] = 1;
  else
+
elseif trackmsg == "error" then
    return mw.text.trim(arg)
+
track[categories.error] = 1;
  end
+
end
end
+
 
local function trimArg2(arg)
+
return '<span style="font-size:100%" class="error citation-comment">' .. msg .. '</span>'
  if arg == nil then
 
    return nil
 
  else
 
    return mw.text.trim(arg)
 
  end
 
 
end
 
end
 +
  
 
--[[--------------------------< base62 >-----------------------
 
--[[--------------------------< base62 >-----------------------
  
    Convert base-62 to base-10
+
Convert base-62 to base-10
    Credit: https://de.wikipedia.org/wiki/Modul:Expr  
+
Credit: https://de.wikipedia.org/wiki/Modul:Expr  
  
  ]]
+
]]
  
 
local function base62( value )
 
local function base62( value )
 +
local r = 1 -- default return value is input value is malformed
  
    local r = 1
+
if value:match ('%W') then -- value must only be in the set [0-9a-zA-Z]
 +
return; -- nil return when value contains extraneous characters
 +
end
  
    if value:match( "^%w+$" ) then
+
local n = #value -- number of characters in value
        local n = #value
+
local k = 1
        local k = 1
+
local c
        local c
+
r = 0
        r = 0
+
for i = n, 1, -1 do -- loop through all characters in value from ls digit to ms digit
        for i = n, 1, -1 do
+
c = value:byte( i, i )
            c = value:byte( i, i )
+
if c >= 48 and c <= 57 then -- character is digit 0-9
            if c >= 48 and c <= 57 then
+
c = c - 48
                c = c - 48
+
elseif c >= 65 and c <= 90 then -- character is ascii a-z
            elseif c >= 65 and c <= 90 then
+
c = c - 55
                c = c - 55
+
else -- must be ascii A-Z
            elseif c >= 97  and  c <= 122 then
+
c = c - 61
                c = c - 61
+
end
            else    -- How comes?
+
r = r + c * k -- accumulate this base62 character's value
                r = 1
+
k = k * 62 -- bump for next
                break    -- for i
+
end -- for i
            end
+
 
            r = r + c * k
+
return r
            k = k * 62
 
        end -- for i
 
    end
 
    return r
 
 
end  
 
end  
  
--[[--------------------------< tableLength >-----------------------
 
  
      Given a 1-D table, return number of elements
+
--[[--------------------------< D E C O D E _ D A T E >--------------------------------------------------------
 +
 
 +
Given a date string, return it in iso format along with an indicator of the date's format.  Except that month names
 +
must be recognizable as legitimate month names with proper capitalization, and that the date string must match one
 +
of the recognized date formats, no error checking is done here; return nil else
 +
 
 +
]]
 +
 
 +
local function decode_date (date_str)
 +
local patterns = {
 +
['dmy'] = {'^(%d%d?) +([^%s%d]+) +(%d%d%d%d)$', 'd', 'm', 'y'}, -- %a does not recognize unicode combining characters used by some languages
 +
['mdy'] = {'^([^%s%d]+) (%d%d?), +(%d%d%d%d)$', 'm', 'd', 'y'},
 +
['ymd'] = {'^(%d%d%d%d) +([^%s%d]+) (%d%d?)$', 'y', 'm', 'd'}, -- not mos compliant at en.wiki but may be acceptible at other wikis
 +
};
 +
 +
local t = {};
 +
 
 +
if non_western_digits then -- this wiki uses non-western digits?
 +
date_str = mw.ustring.gsub (date_str, '%d', digits); -- convert this wiki's non-western digits to western digits
 +
end
  
  ]]
+
if date_str:match ('^%d%d%d%d%-%d%d%-%d%d$') then -- already an iso format date, return western digits form
 +
return date_str, 'iso';
 +
end
 +
 +
for k, v in pairs (patterns) do
 +
local c1, c2, c3 = mw.ustring.match (date_str, patterns[k][1]); -- c1 .. c3 are captured but we don't know what they hold
 +
 +
if c1 then -- set on match
 +
t = { -- translate unspecified captures to y, m, and d
 +
[patterns[k][2]] = c1, -- fill the table of captures with the captures
 +
[patterns[k][3]] = c2, -- take index names from src_pattern table and assign sequential captures
 +
[patterns[k][4]] = c3,
 +
};
 +
if month_num[t.m] then -- when month not already a number
 +
t.m = month_num[t.m]; -- replace valid month name with a number
 +
else
 +
return nil, 'iso'; -- not a valid date form because month not valid
 +
end
  
local function tableLength(T)
+
return mw.ustring.format ('%.4d-%.2d-%.2d', t.y, t.m, t.d), k; -- return date in iso format
  local count = 0
+
end
  for _ in pairs(T) do count = count + 1 end
+
end
  return count
+
return nil, 'iso'; -- date could not be decoded; return nil and default iso date
 
end
 
end
  
 +
 +
--[[--------------------------< makeDate >-----------------------
  
--[[--------------------------< dateFormat >-----------------------
+
Given year, month, day numbers, (zero-padded or not) return a full date in df format
 +
where df may be one of:
 +
mdy, dmy, iso, ymd
  
    Given a date string, return its format: dmy, mdy, iso, ymd
+
on entry, year, month, day are presumed to be correct for the date that they represent; all are required
      If unable to determine return nil
 
  
  ]]
+
in this module, makeDate() is sometimes given an iso-format date in year:
 +
makeDate (2018-09-20, nil, nil, df)
 +
this works because table.concat() sees only one table member
  
local function dateFormat(date)
+
]]
  
  local dt = {}
+
local function makeDate (year, month, day, df)
  dt.split = {}
+
local format = {
 +
['dmy'] = 'j F Y',
 +
['mdy'] = 'F j, Y',
 +
['ymd'] = 'Y F j',
 +
['iso'] = 'Y-m-d',
 +
};
  
  dt.split = mw.text.split(date, "-")
+
local date = table.concat ({year, month, day}, '-'); -- assemble year-initial numeric-format date (zero padding not required here)
  if tableLength(dt.split) == 3 then
 
    if tonumber(dt.split[1]) > 1900 and tonumber(dt.split[1]) < 2200 and tonumber(dt.split[2]) and tonumber(dt.split[3]) then
 
      return "iso"
 
    else
 
      return nil
 
    end
 
  end 
 
  
  dt.split = mw.text.split(date, " ")
+
if non_western_digits then -- this wiki uses non-western digits?
  if tableLength(dt.split) == 3 then
+
date = mw.ustring.gsub (date, '%d', digits); -- convert this wiki's non-western digits to western digits
    if tonumber(dt.split[3]) then
+
end
      if tonumber(dt.split[3]) > 1900 and tonumber(dt.split[3]) < 2200 then
 
        if tonumber(dt.split[1]) then
 
          return "dmy"
 
        else
 
          return "mdy"
 
        end  
 
      else
 
        if tonumber(dt.split[1]) then
 
          if tonumber(dt.split[1]) > 1900 and tonumber(dt.split[1]) < 2200 then
 
            return "ymd"
 
          end
 
        end
 
      end
 
    end
 
  end
 
  return nil
 
  
 +
return mw.getContentLanguage():formatDate (format[df], date);
 
end
 
end
  
--[[--------------------------< makeDate >-----------------------
 
  
    Given a zero-padded 4-digit year, 2-digit month and 2-digit day, return a full date in df format
+
--[[--------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------
    df = mdy, dmy, iso, ymd
+
 
 +
Returns true if date is after 31 December 1899 (why is 1900 the min year? shouldn't the internet's date-of-birth
 +
be min year?), not after today's date, and represents a valid date (29 February 2017 is not a valid date).  Applies
 +
Gregorian leapyear rules.
  
]]
+
all arguments are required
  
local function makeDate(year, month, day, df)
+
]]
  
  if not year or year == "" or not month or month == "" or not day or day == "" then
+
local function is_valid_date (year, month, day)
    return nil
+
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  end
+
local month_length;
 +
local y, m, d;
 +
local today = os.date ('*t'); -- fetch a table of current date parts
  
  local zmonth = month                                                     -- month with leading 0
+
if not year or '' == year or not month or '' == month or not day or '' == day then
  month = month:match("0*(%d+)")                                            -- month without leading 0
+
return false; -- something missing
  if tonumber(month) < 1 or tonumber(month) > 12 then
+
end
    return year
+
  end
+
y = tonumber (year);
  local nmonth = os.date("%B", os.time{year=2000, month=month, day=1} )     -- month in name form     
+
m = tonumber (month);
  if not nmonth then
+
d = tonumber (day);
    return year
 
  end
 
  
  local zday = day
+
if 1900 > y or today.year < y or 1 > m or 12 < m then -- year and month are within bounds TODO: 1900?
  day = zday:match("0*(%d+)")
+
return false;
  if tonumber(day) < 1 or tonumber(day) > 31 then
+
end
    if df == "mdy" or df == "dmy" then
 
      return nmonth .. " " .. year
 
    elseif df == "iso" then
 
      return year .. "-" .. zmonth
 
    elseif df == "ymd" then
 
      return year .. " " .. nmonth
 
    else
 
      return nmonth .. " " .. year
 
    end
 
  end                                      
 
  
  if df == "mdy" then
+
if (2==m) then -- if February
    return nmonth .. " " .. day .. ", " .. year        -- September 1, 2016
+
month_length = 28; -- then 28 days unless
  elseif df == "dmy" then
+
if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then -- is a leap year?
    return day .. " " .. nmonth .. " " .. year          -- 1 September 2016
+
month_length = 29; -- if leap year then 29 days in February
  elseif df == "iso" then
+
end
    return year .. "-" .. zmonth .. "-" .. zday        -- 2016-09-01
+
else
  elseif df == "ymd" then
+
month_length=days_in_month[m];
    return year .. " " .. nmonth .. " " .. cday          -- 2016 September 1
+
end
  else
 
    return nmonth .. " " .. day .. ", " .. year        -- September 1, 2016
 
  end
 
  
 +
if 1 > d or month_length < d then -- day is within bounds
 +
return false;
 +
end
 +
-- here when date parts represent a valid date
 +
return os.time({['year']=y, ['month']=m, ['day']=d, ['hour']=0}) <= os.time(); -- date at midnight must be less than or equal to current date/time
 
end
 
end
  
Line 208: Line 323:
 
--[[--------------------------< decodeWebciteDate >-----------------------
 
--[[--------------------------< decodeWebciteDate >-----------------------
  
      Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format
+
Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format
  
  ]]
+
returns date string in df format - webcite date is a unix timestamp encoded as bae62
local function decodeWebciteDate(path, df)
+
or the string 'query'
  
    local dt = {}
+
]]
    dt.split = {}
 
  
    dt.split = mw.text.split(path, "/")
+
local function decodeWebciteDate(path, df)
  
    -- valid URL formats that are not base62
+
local dt = {};
 +
local decode;
  
    -- http://www.webcitation.org/query?id=1138911916587475
+
dt = mw.text.split(path, "/")
    -- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
 
    -- http://www.webcitation.org/1138911916587475
 
    -- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
 
    -- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e
 
  
    if mw.ustring.find( dt.split[2], "query", 1, plain) or
+
-- valid URL formats that are not base62
      mw.ustring.find( dt.split[2], "cache", 1, plain) or
 
      mw.ustring.find( dt.split[2], "getfile", 1, plain) or
 
      tonumber(dt.split[2]) then
 
      return "query"
 
    end
 
  
    dt.full = os.date("%Y %m %d", string.sub(string.format("%d", base62(dt.split[2])),1,10) )
+
-- http://www.webcitation.org/query?id=1138911916587475
    dt.split = mw.text.split(dt.full, " ")
+
-- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
    dt.year = dt.split[1]
+
-- http://www.webcitation.org/1138911916587475
    dt.month = dt.split[2]
+
-- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
    dt.day = dt.split[3]
+
-- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e
  
    if not tonumber(dt.year) or not tonumber(dt.month) or not tonumber(dt.day) then
+
if dt[2]:find ('query', 1, true) or
      return inlineRed("[Date error] (1)", "error")
+
dt[2]:find ('cache', 1, true) or
    end
+
dt[2]:find ('getfile', 1, true) or
 +
tonumber(dt[2]) then
 +
return 'query';
 +
end
  
    if tonumber(dt.month) > 12 or tonumber(dt.day) > 31 or tonumber(dt.month) < 1 then
+
decode = base62(dt[2]); -- base62 string -> exponential number
      return inlineRed("[Date error] (2)", "error")
+
if not decode then
    end
+
return nil; -- nil return when dt[2] contains characters not in %w
    if tonumber(dt.year) > tonumber(os.date("%Y")) or tonumber(dt.year) < 1900 then
+
end
      return inlineRed("[Date error] (3)", "error")
+
dt = os.date('*t', string.format("%d", decode):sub(1,10)) -- exponential number -> text -> first 10 characters (a unix timestamp) -> a table of date parts
    end
 
  
    fulldate = makeDate(dt.year, dt.month, dt.day, df)
+
decode = makeDate (dt.year, dt.month, dt.day, 'iso'); -- date comparisons are all done in iso format with western digits
    if not fulldate then
+
if non_western_digits then -- this wiki uses non-western digits?
      return inlineRed("[Date error] (4)", "error")
+
decode = mw.ustring.gsub (decode, '%d', digits); -- convert this wiki's non-western digits to western digits
    else
+
end
      return fulldate
 
    end
 
  
 +
return decode;
 
end
 
end
 +
  
 
--[[--------------------------< decodeWaybackDate >-----------------------
 
--[[--------------------------< decodeWaybackDate >-----------------------
  
 
Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
 
Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
  return the formatted date eg. "September 1, 2016" in df format
+
or Library of Congress Web Archives (eg. /all/20160901010101/http://example.com)
  Handle non-digits in snapshot ID such as "re_" and "-" and "*"
+
or UK Government Web Archive (eg. /ukgwa/20160901010101/http://example.com or /tna/20160901010101/http://example.com)
  
]]
+
return the formatted date eg. "September 1, 2016" in df format
 +
Handle non-digits in snapshot ID such as "re_" and "-" and "*"
 +
 
 +
returns two values:
 +
first value is one of these:
 +
valid date string in df format - wayback date is valid (including the text string 'index' when date is '/*/')
 +
empty string - wayback date is malformed (less than 8 digits, not a valid date)
 +
nil - wayback date is '/save/' or otherwise not a number
 +
 +
second return value is an appropriate 'message' may or may not be formatted
 +
 
 +
]]
  
 
local function decodeWaybackDate(path, df)
 
local function decodeWaybackDate(path, df)
  
    local snapdate, snapdatelong, currdate, fulldate
+
local msg, snapdate;
 +
 
 +
snapdate = path:gsub ('^/web/', ''):gsub ('^/all/', ''):gsub ('^/ukgwa/', ''):gsub ('^/tna/', ''):gsub ('^/', ''); -- remove leading /web/, /all/, /ukgwa/, /tna/, or /
 +
snapdate = snapdate:match ('^[^/]+'); -- get timestamp
 +
if snapdate == "*" then -- eg. /web/*/http.., etc.
 +
return 'index'; -- return indicator that this url has an index date
 +
end
 +
 
 +
snapdate = snapdate:gsub ('%a%a_%d?$', ''):gsub ('%-', ''); -- from date, remove any trailing "re_", dashes
 +
 
 +
msg = '';
 +
if snapdate:match ('%*$') then -- a trailing '*' causes calendar display at archive .org
 +
snapdate = snapdate:gsub ('%*$', ''); -- remove so not part of length calc later
 +
msg = inlineRed (err_warn_msgs.ts_cal, 'warning'); -- make a message
 +
end
 +
 
 +
if not tonumber(snapdate) then
 +
return nil, 'ts_nan'; -- return nil (fatal error flag) and message selector
 +
end
  
    local safe = path
+
local dlen = snapdate:len();
    snapdate = string.gsub(safe, "^/w?e?b?/?", "")                     -- Remove leading "/web/" or "/"
+
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
    safe = snapdate
+
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
    local N = mw.text.split(safe, "/")
+
end
    snapdate = N[1]
 
    if snapdate == "*" then                                             -- eg. /web/*/http..
 
      return "index"
 
    end
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[a-z][a-z]_[0-9]?$", "")             -- Remove any trailing "re_" from date
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[-]", "")                            -- Remove dashes from date eg. 2015-01-01
 
    safe = snapdate
 
    snapdate = string.gsub(safe, "[*]$", "")                            -- Remove trailing "*"
 
  
    if not tonumber(snapdate) then
+
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here
      return inlineRed("[Date error] (2)", "error")
 
    end
 
    local dlen = string.len(snapdate)
 
    if dlen < 4 then
 
      return inlineRed("[Date error] (3)", "error")
 
    end
 
    if dlen < 14 then
 
      snapdatelong = snapdate .. string.rep("0", 14 - dlen)
 
    else
 
      snapdatelong = snapdate
 
    end
 
    local year = string.sub(snapdatelong, 1, 4)
 
    local month = string.sub(snapdatelong, 5, 6)
 
    local day = string.sub(snapdatelong, 7, 8)
 
    if not tonumber(year) or not tonumber(month) or not tonumber(day) then
 
      return inlineRed("[Date error] (4)", "error")
 
    end
 
    if tonumber(month) > 12 or tonumber(day) > 31 or tonumber(month) < 1 then
 
      return inlineRed("[Date error] (5)", "error")
 
    end
 
    currdate = os.date("%Y")
 
    if tonumber(year) > tonumber(currdate) or tonumber(year) < 1900 then
 
      return inlineRed("[Date error] (6)", "error")
 
    end
 
  
    fulldate = makeDate(year, month, day, df)
+
if not is_valid_date (year, month, day) then
    if not fulldate then
+
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
      return inlineRed("[Date error] (7)", "error")
+
end
    else
 
      return fulldate
 
    end
 
  
 +
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
 +
if 14 == dlen then
 +
return snapdate, msg; -- return date with message if any
 +
else
 +
return snapdate, msg .. inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message(s)
 +
end
 
end
 
end
 +
  
 
--[[--------------------------< decodeArchiveisDate >-----------------------
 
--[[--------------------------< decodeArchiveisDate >-----------------------
  
  Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com)
+
Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com)
  return the date in df format (e.g. if df = dmy, return 28 August 2016)
+
return the date in df format (e.g. if df = dmy, return 28 August 2016)
  Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552
+
Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552
 +
 
 +
returns two values:
 +
first value is one of these:
 +
valid date string in df format - archive.is date is valid (including the text string 'short link' when url is the short form)
 +
empty string - wayback date is malformed (not a number, less than 8 digits, not a valid date)
 +
nil - wayback date is '/save/'
 +
 +
second return value is an appropriate 'message' may or may not be formatted
  
  ]]
+
]]
  
 
local function decodeArchiveisDate(path, df)
 
local function decodeArchiveisDate(path, df)
 +
local snapdate
  
    local snapdate, snapdatelong, currdate, fulldate
+
if path:match ('^/%w+$') then -- short form url path is '/' followed by some number of base 62 digits and nothing else
 +
return "short link" -- e.g. http://archive.is/hD1qz
 +
end
  
    local safe = path
+
snapdate = mw.text.split (path, '/')[2]:gsub('[%.%-]', ''); -- get snapshot date, e.g. 2016.08.28-144552; remove periods and hyphens
    local N = mw.text.split(safe, "/")
 
    safe = N[2]                                                         -- get snapshot date, e.g. 2016.08.28-144552
 
    snapdate = string.gsub(safe, "[%.%-]", "")                          -- remove periods and hyphens
 
  
    if not tonumber(snapdate) then                                     -- if not numeric, it is "short link", not date
+
local dlen = string.len(snapdate)
        return "short link"                                            -- e.g. http://archive.is/hD1qz
+
if dlen < 8 then -- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
    end
+
return '', inlineRed (err_warn_msgs.ts_short, 'error'); -- return empty string and error message
 +
end
  
    local dlen = string.len(snapdate)
+
local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)'); -- no need for snapdatelong here
    if dlen < 4 then
 
        return inlineRed("[Date error] (3)", "error")
 
    end
 
    if dlen < 14 then
 
        snapdatelong = snapdate .. string.rep("0", 14 - dlen)
 
    else
 
        snapdatelong = snapdate
 
    end
 
    local year = string.sub(snapdatelong, 1, 4)
 
    local month = string.sub(snapdatelong, 5, 6)
 
    local day = string.sub(snapdatelong, 7, 8)
 
    if not tonumber(year) or not tonumber(month) or not tonumber(day) then
 
        return inlineRed("[Date error] (4)", "error")
 
    end
 
    if tonumber(month) > 12 or tonumber(day) > 31 or tonumber(month) < 1 then
 
        return inlineRed("[Date error] (5)", "error")
 
    end
 
    currdate = os.date("%Y")
 
    if tonumber(year) > tonumber(currdate) or tonumber(year) < 1900 then
 
        return inlineRed("[Date error] (6)", "error")
 
    end
 
  
    fulldate = makeDate(year, month, day, df)
+
if not is_valid_date (year, month, day) then
    if not fulldate then
+
return '', inlineRed (err_warn_msgs.ts_date, 'error'); -- return empty string and error message
        return inlineRed("[Date error] (7)", "error")
+
end
    else
 
        return fulldate
 
    end
 
  
 +
snapdate = table.concat ({year, month, day}, '-'); -- date comparisons are all done in iso format
 +
if 14 == dlen then
 +
return snapdate; -- return date
 +
else
 +
return snapdate, inlineRed (err_warn_msgs.ts_len, 'warning'); -- return date with warning message
 +
end
 
  end
 
  end
  
Line 377: Line 475:
 
--[[--------------------------< serviceName >-----------------------
 
--[[--------------------------< serviceName >-----------------------
  
    Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID
+
Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID
  
  ]]
+
]]
  
local function serviceName(host, nolink)
+
local function serviceName(host, no_link)
 +
local tracking;
 +
local index;
 +
 +
host = host:lower():gsub ('^web%.(.+)', '%1'):gsub ('^www%.(.+)', '%1'); -- lowercase, remove web. and www. subdomains
  
  local tracking = "Category:Webarchive template other archives"
+
if services[host] then
 +
index = host;
 +
else
 +
for k, _ in pairs (services) do
 +
if host:find ('%f[%a]'..k:gsub ('([%.%-])', '%%%1')) then
 +
index = k;
 +
break;
 +
end
 +
end
 +
end
 +
 +
if index then
 +
local out = {''}; -- empty string in [1] so that concatenated result has leading single space
 +
ulx.url1.service = services[index][4] or 'other';
 +
tracking = services[index][5] or categories.other;
 +
-- build tail string
 +
if false == services[index][1] then -- select prefix
 +
table.insert (out, prefixes.at);
 +
elseif true == services[index][1] then
 +
table.insert (out, prefixes.atthe);
 +
else
 +
table.insert (out, services[index][1]);
 +
end
 +
 +
table.insert (out, make_wikilink (services[index][2], services[index][3], no_link)); -- add article wikilink
 +
if services[index][6] then -- add tail postfix if it exists
 +
table.insert (out, services[index][6]);
 +
end
 +
 +
ulx.url1.tail = table.concat (out, ' '); -- put it all together; result has leading space character
  
  local bracketopen = "[["
+
else -- here when unknown archive
  local bracketclose = "]]"
+
ulx.url1.service = 'other';
  if nolink then
+
tracking = categories.unknown;
    bracketopen = ""
+
ulx.url1.tail = table.concat ({'', prefixes.at, host, inlineRed (err_warn_msgs.unknown_url, error)}, ' ');
    bracketclose = ""
+
end
  end
+
 +
track[tracking] = 1
 +
end
  
  ulx.url1.service = "other"
 
  ulx.url1.tail = " at " .. ulx.url1.host .. " " .. inlineRed("Error: unknown archive URL")
 
  
  host = string.lower(host)
+
--[[--------------------------< parseExtraArgs >-----------------------
  
  if mw.ustring.find( host, "europarchive.org", 1, plain ) then  -- any containing "archive.org" listed before Wayback to avoid disambiguation
+
Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10
    ulx.url1.tail = " at the " .. bracketopen .. "National Library of Ireland" .. bracketclose
+
For example: {{webarchive |url=.. |url4=.. |url7=..}}
  elseif mw.ustring.find( host, "webarchive.org.uk", 1, plain ) then
+
Three url arguments not in numeric sequence (1..4..7).  
    ulx.url1.tail = " at the " .. bracketopen .. "UK Web Archive" .. bracketclose
+
Function only processes arguments numbered 2 or greater (in this case 4 and 7)
  elseif mw.ustring.find( host, "archive.org", 1, plain ) then
+
It creates numeric sequenced table entries like:
    ulx.url1.service = "wayback"
+
urlx.url2.url = <argument value for url4>
    ulx.url1.tail = " at the " .. bracketopen .. "Wayback Machine" .. bracketclose
+
urlx.url3.url = <argument value for url7>
    tracking = "Category:Webarchive template wayback links"
+
Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")
  elseif mw.ustring.find( host, "webcitation.org", 1, plain ) then
 
    ulx.url1.service = "webcite"
 
    ulx.url1.tail = " at " .. bracketopen .. "WebCite" .. bracketclose
 
    tracking = "Category:Webarchive template webcite links"
 
  elseif mw.ustring.find( host, "archive.is", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.fo", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.today", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.li", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive.ec", 1, plain ) then
 
    ulx.url1.service = "archiveis"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive.is" .. bracketclose
 
    tracking = "Category:Webarchive template archiveis links"
 
  elseif mw.ustring.find( host, "archive[-]it.org", 1, plain ) then
 
    ulx.url1.service = "archiveit"
 
    ulx.url1.tail = " at " .. bracketopen .. "Archive-It" .. bracketclose
 
  elseif mw.ustring.find( host, "arquivo.pt", 1, plain) then
 
    ulx.url1.tail = " at the " .. "Portuguese Web Archive"
 
  elseif mw.ustring.find( host, "loc.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "Library of Congress" .. bracketclose
 
  elseif mw.ustring.find( host, "webharvest.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "National Archives and Records Administration" .. bracketclose
 
  elseif mw.ustring.find( host, "bibalex.org", 1, plain ) then
 
    ulx.url1.tail = " at " .. "[[Bibliotheca_Alexandrina#Internet_Archive_partnership|Bibliotheca Alexandrina]]"
 
  elseif mw.ustring.find( host, "collectionscanada", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Canadian Government Web Archive"
 
  elseif mw.ustring.find( host, "haw.nsk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Croatian Web Archive (HAW)"
 
  elseif mw.ustring.find( host, "veebiarhiiv.digar.ee", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Estonian Web Archive"
 
  elseif mw.ustring.find( host, "vefsafn.is", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "[[National and University Library of Iceland]]"
 
  elseif mw.ustring.find( host, "proni.gov", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "Public Record Office of Northern Ireland" .. bracketclose
 
  elseif mw.ustring.find( host, "uni[-]lj.si", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "Slovenian Web Archive"
 
  elseif mw.ustring.find( host, "stanford.edu", 1, plain ) then
 
    ulx.url1.tail = " at the " .. "[[Stanford University Libraries|Stanford Web Archive]]"
 
  elseif mw.ustring.find( host, "nationalarchives.gov.uk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "UK Government Web Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "parliament.uk", 1, plain ) then
 
    ulx.url1.tail = " at the " .. bracketopen .. "UK Parliament's Web Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "nlb.gov.sg", 1, plain ) then
 
    ulx.url1.tail = " at " .. "Web Archive Singapore"
 
  elseif mw.ustring.find( host, "pandora.nla.gov.au", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Pandora Archive" .. bracketclose
 
  elseif mw.ustring.find( host, "perma.cc", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Perma.cc" .. bracketclose
 
  elseif mw.ustring.find( host, "perma-archives.cc", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Perma.cc" .. bracketclose
 
  elseif mw.ustring.find( host, "screenshots.com", 1, plain ) then
 
    ulx.url1.tail = " at Screenshots"
 
  elseif mw.ustring.find( host, "wikiwix.com", 1, plain ) then
 
    ulx.url1.tail = " at Wikiwix"
 
  elseif mw.ustring.find( host, "freezepage.com", 1, plain ) then
 
    ulx.url1.tail = " at Freezepage"
 
  elseif mw.ustring.find( host, "webcache.googleusercontent.com", 1, plain ) then
 
    ulx.url1.tail = " at Google Cache"
 
  elseif mw.ustring.find( host, "timetravel.mementoweb.org", 1, plain ) then
 
    ulx.url1.tail = " at " .. bracketopen .. "Memento Project" .. bracketclose
 
  else
 
    tracking = "Category:Webarchive template unknown archives"
 
  end
 
  
  track[tracking] = 1
+
]]
  
 +
local function parseExtraArgs(args)
 +
 +
local i, j, argurl, argurl2, argdate, argtitle
 +
 +
j = 2
 +
for i = 2, config.maxurls do
 +
argurl = "url" .. i
 +
if args[argurl] then
 +
argurl2 = "url" .. j
 +
ulx[argurl2] = {}
 +
ulx[argurl2]["url"] = args[argurl]
 +
argdate = "date" .. j
 +
if args[argdate] then
 +
ulx[argurl2]["date"] = args[argdate]
 +
else
 +
ulx[argurl2]["date"] = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
end
 +
 +
argtitle = "title" .. j
 +
if args[argtitle] then
 +
ulx[argurl2]["title"] = args[argtitle]
 +
else
 +
ulx[argurl2]["title"] = nil
 +
end
 +
j = j + 1
 +
end
 +
end
 +
 +
if j == 2 then
 +
return 0
 +
else
 +
return j - 2
 +
end
 
end
 
end
  
--[[--------------------------< parseExtraArgs >-----------------------
 
  
    Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10
+
--[[--------------------------< comma >-----------------------
      For example: {{webarchive |url=.. |url4=.. |url7=..}}
+
 
        Three url arguments not in numeric sequence (1..4..7).
+
Given a date string, return "," if it's MDY
        Function only processes arguments numbered 2 or greater (in this case 4 and 7)
+
 
        It creates numeric sequenced table entries like:
+
]]
          urlx.url2.url = <argument value for url4>
+
 
          urlx.url3.url = <argument value for url7>
+
local function comma(date)
      Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")
+
return (date and date:match ('%a+ +%d%d?(,) +%d%d%d%d')) or '';
 +
end
 +
 
  
]]
+
--[[--------------------------< createRendering >-----------------------
  
local function parseExtraArgs()
+
Return a rendering of the data in ulx[][]
  
  local i, j, argurl, argurl2, argdate, argtitle
+
]]
  
  j = 2
+
local function createRendering()
  for i = 2, maxurls do
 
    argurl = "url" .. i
 
    if trimArg(args[argurl]) then
 
      argurl2 = "url" .. j
 
      ulx[argurl2] = {}
 
      ulx[argurl2]["url"] = args[argurl]
 
      argdate = "date" .. j
 
      if trimArg(args[argdate]) then
 
        ulx[argurl2]["date"] = args[argdate]
 
      else
 
        ulx[argurl2]["date"] = inlineRed("[Date missing]", "warning")
 
      end
 
      argtitle = "title" .. j
 
      if trimArg(args[argtitle]) then
 
        ulx[argurl2]["title"] = args[argtitle]
 
      else
 
        ulx[argurl2]["title"] = nil
 
      end
 
      j = j + 1
 
    end
 
  end
 
  
  if j == 2 then
+
local displayfield
    return 0
+
local out = {};
  else
+
    return j - 2
+
local index_date, msg = ulx.url1.date:match ('(index)(.*)'); -- when ulx.url1.date extract 'index' text and message text (if there is a message)
  end
+
ulx.url1.date = ulx.url1.date:gsub ('index.*', 'index'); -- remove message
  
end
+
if 'none' == ulx.url1.format then -- For {{wayback}}, {{webcite}}
 +
table.insert (out, '['); -- open extlink markup
 +
table.insert (out, ulx.url1.url); -- add url
 +
 
 +
if ulx.url1.title then
 +
table.insert (out, ' ') -- the required space
 +
table.insert (out, ulx.url1.title) -- the title
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, ulx.url1.tail); -- tail text
 +
if ulx.url1.date then
 +
table.insert (out, '&#32;('); -- open date text; TODO: why the html entity? replace with regular space?
 +
table.insert (out, 'index' == ulx.url1.date and s_text.archive or s_text.archived); -- add text
 +
table.insert (out, ' '); -- insert a space
 +
table.insert (out, ulx.url1.date); -- add date
 +
table.insert (out, ')'); -- close date text
 +
end
 +
else -- no title
 +
if index_date then -- when url date is 'index'
 +
table.insert (out, table.concat ({' ', s_text.Archive_index, ']'})); -- add the index link label
 +
table.insert (out, msg or ''); -- add date mismatch message when url date is /*/ and |date= has valid date
 +
else
 +
table.insert (out, table.concat ({' ', s_text.Archived, '] '})); -- add link label for url has timestamp date (will include mismatch message if there is one)
 +
end
 +
if ulx.url1.date then
 +
if 'index' ~= ulx.url1.date then
 +
table.insert (out, ulx.url1.date); -- add date when data is not 'index'
 +
end
 +
table.insert (out, comma(ulx.url1.date)); -- add ',' if date format is mdy
 +
table.insert (out, ulx.url1.tail); -- add tail text
 +
else -- no date
 +
table.insert (out, ulx.url1.tail); -- add tail text
 +
end
 +
end
 +
 
 +
if 0 < ulx.url1.extraurls then -- For multiple archive URLs
 +
local tot = ulx.url1.extraurls + 1
 +
table.insert (out, '.') -- terminate first url
 +
table.insert (out, table.concat ({' ', s_text.addlarchives, ': '})); -- add header text
  
--[[--------------------------< comma >-----------------------
+
for i=2, tot do -- loop through the additionals
 +
local index = table.concat ({'url', i}); -- make an index
 +
displayfield = ulx[index]['title'] and 'title' or 'date'; -- choose display text
 +
table.insert (out, '['); -- open extlink markup
 +
table.insert (out, ulx[index]['url']); -- add the url
 +
table.insert (out, ' '); -- the required space
 +
table.insert (out, ulx[index][displayfield]); -- add the label
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, i==tot and '.' or ', '); -- add terminator
 +
end
 +
end
 +
return table.concat (out); -- make a big string and done
  
    Given a date string, return "," if it's MDY
+
else -- For {{cite archives}}
 +
if 'addlarchives' == ulx.url1.format then -- Multiple archive services
 +
table.insert (out, table.concat ({s_text.addlarchives, ': '})); -- add header text
 +
else -- Multiple pages from the same archive
 +
table.insert (out, table.concat ({s_text.addlpages, ' '})); -- add header text
 +
table.insert (out, ulx.url1.date); -- add date to header text
 +
table.insert (out, ': '); -- close header text
 +
end
  
  ]]
+
local tot = ulx.url1.extraurls + 1;
 +
for i=1, tot do -- loop through the additionals
 +
local index = table.concat ({'url', i}); -- make an index
 +
table.insert (out, '['); -- open extlink markup
 +
table.insert (out, ulx[index]['url']); -- add url
 +
table.insert (out, ' '); -- add required space
  
local function comma(date)
+
displayfield = ulx[index]['title'];
  local N = mw.text.split(date, " ")
+
if 'addlarchives' == ulx.url1.format then
  local O = mw.text.split(N[1], "-") -- for ISO
+
if not displayfield then
  if O[1] == "index" then return "" end
+
displayfield = ulx[index]['date']
  if not tonumber(O[1]) then
+
end
    return ","
+
else -- must be addlpages
  else
+
if not displayfield then
    return ""
+
displayfield = table.concat ({s_text.Page, ' ', i});
  end
+
end
 +
end
 +
table.insert (out, displayfield); -- add title, date, page label text
 +
table.insert (out, ']'); -- close extlink markup
 +
table.insert (out, (i==tot and '.' or ', ')); -- add terminator
 +
end
 +
return table.concat (out); -- make a big string and done
 +
end
 
end
 
end
  
--[[--------------------------< createTracking >-----------------------
 
  
    Return data in track[] ie. tracking categories
+
--[[--------------------------< P A R A M E T E R _ N A M E _ X L A T E >--------------------------------------
 +
 
 +
for internaltionalization, translate local-language parameter names to their English equivalents
 +
 
 +
TODO: return error message if multiple aliases of the same canonical parameter name are found?
 +
 
 +
returns two tables:
 +
new_args - holds canonical form parameters and their values either from translation or because the parameter was already in canonical form
 +
origin - maps canonical-form parameter names to their untranslated (local language) form for error messaging in the local language
  
  ]]
+
unrecognized parameters are ignored
  
local function createTracking()
+
]]
  
  local sand = ""
+
local function parameter_name_xlate (args, params, enum_params)
  if tableLength(track) > 0 then                       
+
local name; -- holds modifiable name of the parameter name during evaluation
    for key,_ in pairs(track) do
+
local enum; -- for enumerated parameters, holds the enumerator during evaluation
      sand = sand .. "[[" .. key .. "]]"
+
local found = false; -- flag used to break out of nested for loops
    end
+
local new_args = {}; -- a table that holds canonical and translated parameter k/v pairs
  end
+
local origin = {}; -- a table that maps original (local language) parameter names to their canonical name for local language error messaging
  return sand
+
local unnamed_params; -- set true when unsupported positional parameters are detected
 +
 +
for k, v in pairs (args) do -- loop through all of the arguments in the args table
 +
name = k; -- copy of original parameter name
  
 +
if 'string' == type (k) then
 +
if non_western_digits then -- true when non-western digits supported at this wiki
 +
name = mw.ustring.gsub (name, '%d', digits); -- convert this wiki's non-western digits to western digits
 +
end
 +
 +
enum = name:match ('%d+$'); -- get parameter enumerator if it exists; nil else
 +
 +
if not enum then -- no enumerator so looking for non-enumnerated parameters
 +
-- TODO: insert shortcut here? if params[name] then name holds the canonical parameter name; no need to search further
 +
for pname, aliases in pairs (params) do -- loop through each parameter the params table
 +
for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table
 +
if name == alias then
 +
new_args[pname] = v; -- create a new entry in the new_args table
 +
origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name
 +
found = true; -- flag so that we can break out of these nested for loops
 +
break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair
 +
end
 +
end
 +
 +
if found then -- true when we found an alias that matched name
 +
found = false; -- reset the flag
 +
break; -- go do next args k/v pair
 +
end
 +
end
 +
else -- enumerated parameters
 +
name = name:gsub ('%d$', '#'); -- replace enumeration digits with place holder for table search
 +
-- TODO: insert shortcut here? if num_params[name] then name holds the canonical parameter name; no need to search further
 +
for pname, aliases in pairs (enum_params) do -- loop through each parameter the num_params table
 +
for _, alias in ipairs (aliases) do -- loop through each alias in the parameter's aliases table
 +
if name == alias then
 +
pname = pname:gsub ('#$', enum); -- replace the '#' place holder with the actual enumerator
 +
new_args[pname] = v; -- create a new entry in the new_args table
 +
origin [pname] = k; -- create an entry to make canonical parameter name to original local language parameter name
 +
found = true; -- flag so that we can break out of these nested for loops
 +
break; -- no need to search the rest of the aliases table for name so go on to the next k, v pair
 +
end
 +
end
 +
 +
if found then -- true when we found an alias that matched name
 +
found = false; -- reset the flag
 +
break; -- go do next args k/v pair
 +
end
 +
end
 +
end
 +
else
 +
unnamed_params = true; -- flag for unsupported positional parameters
 +
end
 +
end -- for k, v
 +
return new_args, origin, unnamed_params;
 
end
 
end
  
--[[--------------------------< createRendering >-----------------------
 
  
    Return a rendering of the data in ulx[][]
+
--[[--------------------------< W E B A R C H I V E >----------------------------------------------------------
 +
 
 +
template entry point
 +
 
 +
]]
 +
 
 +
local function webarchive(frame)
 +
local args = getArgs (frame);
 +
 
 +
local data = mw.loadData (table.concat ({ -- make a data module name; sandbox or live
 +
'Module:Webarchive/data',
 +
frame:getTitle():find('sandbox', 1, true) and '/sandbox' or '' -- this instance is ./sandbox then append /sandbox
 +
}));
 +
categories = data.categories; -- fill in the forward declarations
 +
config = data.config;
 +
if data.digits.enable then
 +
digits = data.digits; -- for i18n; table of digits in the local wiki's language
 +
non_western_digits = true; -- use_non_western_digits
 +
end
 +
err_warn_msgs = data.err_warn_msgs;
 +
excepted_pages = data.excepted_pages;
 +
month_num = data.month_num; -- for i18n; table of month names in the local wiki's language
 +
prefixes = data.prefixes;
 +
services = data.services;
 +
s_text = data.s_text;
 +
uncategorized_namespaces = data.uncategorized_namespaces;
 +
uncategorized_subpages = data.uncategorized_subpages;
  
  ]]
+
local origin = {}; -- holds a map of English to local language parameter names used in the current template; not currently used
 +
local unnamed_params; -- boolean set to true when template call has unnamed parameters
 +
args, origin, unnamed_params = parameter_name_xlate (args, data.params, data.enum_params); -- translate parameter names in args to English
  
local function createRendering()
+
local date, format, msg, udate, uri, url;
 +
local ldf = 'iso'; -- when there is no |date= parameter, render url dates in iso format
 +
 +
if args.url and args.url1 then -- URL argument (first)
 +
return inlineError (data.crit_err_msgs.conflicting, {origin.url, origin.url1});
 +
end
 +
 +
url = args.url or args.url1;
 +
 +
if not url then
 +
return inlineError (data.crit_err_msgs.empty);
 +
end
 +
-- these iabot bugs perportedly fixed; removing these causes lua script error
 +
--[[ -- at Template:Webarchive/testcases/Production; resolve that before deleting these tests
 +
if mw.ustring.find( url, "https://web.http", 1, true ) then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
 +
track[categories.error] = 1;
 +
return inlineError (data.crit_err_msgs.iabot1);
 +
end
 +
if url == "https://web.archive.org/http:/" then -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
 +
track[categories.error] = 1;
 +
return inlineError (data.crit_err_msgs.iabot2);
 +
end
 +
]]
  
    local sand, displayheader, displayfield
+
if not (url:lower():find ('^http') or url:find ('^//')) then
 +
return inlineError (data.crit_err_msgs.invalid_url );
 +
end
  
    local period1 = ""  -- For backwards compat with {{wayback}}
+
ulx.url1 = {}
    local period2 = "."                                                           
+
ulx.url1.url = url
 
 
    local indexstr = "archived"
 
    if ulx.url1.date == "index" then
 
      indexstr = "archive"
 
    end 
 
                                                                                          -- For {{wayback}}, {{webcite}}
 
  
    if ulx.url1.format == "none" then                                                   
+
ulx.url1.extraurls = parseExtraArgs(args)
      if not ulx.url1.title and not ulx.url1.date then                                    -- No title. No date
 
        sand = "[" .. ulx.url1.url .. " Archived]" .. ulx.url1.tail
 
      elseif not ulx.url1.title and ulx.url1.date then                                    -- No title. Date.
 
        if ulx.url1.service == "wayback" then
 
          period1 = "."
 
          period2 = ""
 
        end
 
        sand = "[" .. ulx.url1.url .. " Archived] " .. ulx.url1.date .. comma(ulx.url1.date) .. ulx.url1.tail .. period1
 
      elseif ulx.url1.title and not ulx.url1.date then                                    -- Title. No date.
 
        sand = "[" .. ulx.url1.url .. " " .. ulx.url1.title .. "]" .. ulx.url1.tail
 
      elseif ulx.url1.title and ulx.url1.date then                                        -- Title. Date.
 
        sand = "[" .. ulx.url1.url .. " " .. ulx.url1.title .. "]" .. ulx.url1.tail .. "&#32;(" .. indexstr .. " " .. ulx.url1.date .. ")"
 
      else
 
        return nil
 
      end
 
      if ulx.url1.extraurls > 0 then                                                      -- For multiple archive URLs
 
        local tot = ulx.url1.extraurls + 1
 
        sand = sand .. period2 .. " Additional archives: "
 
        for i=2,tot do
 
          local indx = "url" .. i
 
          if ulx[indx]["title"] then
 
            displayfield = "title"
 
          else
 
            displayfield = "date"
 
          end
 
          sand = sand .. "[" .. ulx[indx]["url"] .. " " .. ulx[indx][displayfield] .. "]"
 
          if i == tot then
 
            sand = sand .. "."
 
          else
 
            sand = sand .. ", "
 
          end
 
        end
 
      else
 
        return sand 
 
      end
 
      return sand
 
                                                                                          -- For {{cite archives}}
 
  
    else                                                                 
+
local good = false;
      if ulx.url1.format == "addlarchives" then                          -- Multiple archive services
+
good, uri = pcall (mw.uri.new, ulx.url1.url); -- get a table of uri parts from this url; protected mode to prevent lua error when ulx.url1.url is malformed
        displayheader = "Additional archives: "
+
      else                                                                -- Multiple pages from the same archive
+
if not good or nil == uri.host then -- abandon when ulx.url1.url is malformed
        displayheader = "Additional pages archived&nbsp;on " .. ulx.url1.date .. ": "
+
return inlineError (data.crit_err_msgs.invalid_url);
      end
+
end
      local tot = 1 + ulx.url1.extraurls
+
      local sand = displayheader
+
serviceName(uri.host, args.nolink)
      for i=1,tot do
 
        local indx = "url" .. i
 
        displayfield = ulx[indx]["title"]
 
        if ulx.url1.format == "addlarchives" then
 
          if not displayfield then
 
            displayfield = ulx[indx]["date"]
 
          end
 
        else
 
          if not displayfield then
 
            displayfield = "Page " .. i
 
          end
 
        end
 
        sand = sand .. "[" .. ulx[indx]["url"] .. " " .. displayfield .. "]"
 
        if i == tot then
 
          sand = sand .. "."
 
        else
 
          sand = sand .. ", "
 
        end
 
      end
 
      return sand
 
    end
 
end
 
  
function p.webarchive(frame)
+
if args.date and args.date1 then -- Date argument
  args = frame.args
+
return inlineError (data.crit_err_msgs.conflicting, {origin.date, origin.date1});
  if (args[1]==nil) and (args["url"]==nil) then           -- if no argument provided than check parent template/module args
+
end
    args = frame:getParent().args
+
  end
+
date = args.date or args.date1;
+
date = date and date:gsub (' +', ' '); -- replace multiple spaces with a single space
  local tname = "Webarchive"                              -- name of calling template. Change if template rename.
 
  ulx = {}                                                -- Associative array to hold template data
 
  track = {}                                              -- Associative array to hold tracking categories
 
  maxurls = 10                                            -- Max number of URLs allowed.
 
  local verifydates = "yes"                              -- See documentation. Set "no" to disable.
 
  
                                                          -- URL argument (first)
+
if date and config.verifydates then
 +
if '*' == date then
 +
date = 'index';
 +
ldf = 'iso'; -- set to default format
 +
elseif 'mdy' == date then
 +
date = nil; -- if date extracted from URL,
 +
ldf = 'mdy'; -- then |date=mdy overrides iso
 +
elseif 'dmy' == date then
 +
date = nil; -- if date extracted from URL,
 +
ldf = 'dmy'; -- then |date=dmy overrides iso
 +
elseif 'ymd' == date then
 +
date = nil; -- if date extracted from URL,
 +
ldf = 'ymd'; -- then |date=ymd overrides iso
 +
else
 +
date, ldf = decode_date (date); -- get an iso format date from date and get date's original format
 +
end
 +
end
  
  local url1 = trimArg(args.url) or trimArg(args.url1)         
+
if 'wayback' == ulx.url1.service or 'locwebarchives' == ulx.url1.service or 'ukgwa' == ulx.url1.service then
  if not url1 then
+
if date then
    return inlineError("url", "Empty.") .. createTracking()
+
if config.verifydates then
  end
+
if ldf then
  if mw.ustring.find( url1, "https://web.http", 1, plain ) then    -- track bug
+
udate, msg = decodeWaybackDate (uri.path); -- get the url date in iso format and format of date in |date=; 'index' when wayback url date is *
    track["Category:Webarchive template errors"] = 1
+
if not udate then -- this is the only 'fatal' error return
    return inlineError("url", "https://web.http") .. createTracking()
+
return inlineError (data.crit_err_msgs[msg]);
  end
+
end
  if url1 == "https://web.archive.org/http:/" then                 -- track bug
 
    track["Category:Webarchive template errors"] = 1
 
    return inlineError("url", "Invalid URL") .. createTracking()
 
  end
 
  
  ulx.url1 = {}
+
if udate ~= date then -- date comparison using iso format dates
  ulx.url1.url = url1
+
date = udate;
  local uri1 = mw.uri.new(ulx.url1.url)
+
msg = table.concat ({
  ulx.url1.host = uri1.host
+
inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message
  ulx.url1.extraurls = parseExtraArgs()
+
msg, -- add message if there is one
 +
});
 +
end
 +
end
 +
end
 +
else -- no |date=
 +
udate, msg = decodeWaybackDate (uri.path);
  
                                                          -- Nolink argument
+
if not udate then -- this is the only 'fatal' error return
 +
return inlineError (data.crit_err_msgs[msg]);
 +
end
  
  local nolink = trimArg2(args.nolink)
+
if '' == udate then
 +
date = nil; -- unset
 +
else
 +
date = udate;
 +
end
 +
end
  
  serviceName(uri1.host, nolink)
+
elseif 'webcite' == ulx.url1.service then
 +
if date then
 +
if config.verifydates then
 +
if ldf then
 +
udate = decodeWebciteDate (uri.path); -- get the url date in iso format
 +
if 'query' ~= udate then -- skip if query
 +
if udate ~= date then -- date comparison using iso format dates
 +
date = udate;
 +
msg = table.concat ({
 +
inlineRed (err_warn_msgs.mismatch, 'warning'),
 +
});
 +
end
 +
end
 +
end
 +
end
 +
else
 +
date = decodeWebciteDate( uri.path, "iso" )
 +
if date == "query" then
 +
date = nil; -- unset
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
elseif not date then -- invalid base62 string
 +
date = inlineRed (err_warn_msgs.date1, 'error');
 +
end
 +
end
  
                                                          -- Date argument
+
elseif 'archiveis' == ulx.url1.service then
 +
if date then
 +
if config.verifydates then
 +
if ldf then
 +
udate, msg = decodeArchiveisDate (uri.path) -- get the url date in iso format
 +
if 'short link' ~= udate then -- skip if short link
 +
if udate ~= date then -- date comparison using iso format dates
 +
date = udate;
 +
msg = table.concat ({
 +
inlineRed (err_warn_msgs.mismatch, 'warning'), -- add warning message
 +
msg, -- add message if there is one
 +
});
 +
end
 +
end
 +
end
 +
end
 +
else -- no |date=
 +
udate, msg = decodeArchiveisDate( uri.path, "iso" )
 +
if udate == "short link" then
 +
date = nil; -- unset
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
elseif '' == udate then
 +
date = nil; -- unset
 +
else
 +
date = udate;
 +
end
 +
end
 +
 +
else -- some other service
 +
if not date then
 +
msg = inlineRed (err_warn_msgs.date_miss, 'warning');
 +
end
 +
end
  
  local date = trimArg(args.date) or trimArg(args.date1)
+
if 'index' == date then
  if date == "*" and ulx.url1.service == "wayback" then
+
ulx.url1.date = date .. (msg or ''); -- create index + message (if there is one)
    date = "index"
+
elseif date then
  elseif date and ulx.url1.service == "wayback" and verifydates == "yes" then
+
ulx.url1.date = makeDate (date, nil, nil, ldf) .. (msg or ''); -- create a date in the wiki's local language + message (if there is one)
    local ldf = dateFormat(date)
+
else
    if ldf then
+
ulx.url1.date = msg;
      local udate = decodeWaybackDate( uri1.path, ldf )
+
end
      if udate ~= date then
+
        date = udate .. inlineRed("<sup>[Date mismatch]</sup>", "warning")      
+
format = args.format; -- Format argument
      end
 
    end
 
  elseif date and ulx.url1.service == "webcite" and verifydates == "yes" then
 
    local ldf = dateFormat(date)
 
    if ldf then
 
      local udate = decodeWebciteDate( uri1.path, ldf )
 
      if udate == "query" then -- skip
 
      elseif udate ~= date then
 
        date = udate .. inlineRed("<sup>[Date mismatch]</sup>", "warning")     
 
      end
 
    end
 
  elseif date and ulx.url1.service == "archiveis" and verifydates == "yes" then
 
    local ldf = dateFormat(date)
 
    if ldf then
 
        local udate = decodeArchiveisDate( uri1.path, ldf )
 
        if udate == "short link" then -- skip
 
        elseif udate ~= date then
 
          date = udate .. inlineRed("<sup>[Date mismatch]</sup>", "warning")     
 
        end
 
    end
 
  elseif not date and ulx.url1.service == "wayback" then
 
    date = decodeWaybackDate( uri1.path, "iso" )
 
    if not date then
 
      date = inlineRed("[Date error] (1)", "error")  
 
    end
 
  elseif not date and ulx.url1.service == "webcite" then
 
    date = decodeWebciteDate( uri1.path, "iso" )
 
    if date == "query" then
 
      date = inlineRed("[Date missing]", "warning")
 
    elseif not date then
 
      date = inlineRed("[Date error] (1)", "error")
 
    end
 
  elseif not date and ulx.url1.service == "archiveis" then
 
    date = decodeArchiveisDate( uri1.path, "iso" )
 
    if date == "short link" then
 
        date = inlineRed("[Date missing]", "warning")
 
    elseif not date then
 
        date = inlineRed("[Date error] (1)", "error")
 
    end
 
  elseif not date then
 
    date = inlineRed("[Date missing]", "warning")
 
  end
 
  ulx.url1.date = date
 
  
                                                          -- Format argument
+
if not format then
 +
format = "none"
 +
else
 +
for k, v in pairs (data.format_vals) do -- |format= accepts two specific values loop through a table of those values
 +
local found; -- declare a nil flag
 +
for _, p in ipairs (v) do -- loop through local language variants
 +
if format == p then -- when |format= value matches
 +
format = k; -- use name from table key
 +
found = true; -- declare found so that we can break out of outer for loop
 +
break; -- break out of inner for loop
 +
end
 +
end
 +
 +
if found then
 +
break;
 +
end
 +
end
  
  local format = trimArg(args.format)
+
if format == "addlpages" then
  if not format then
+
if not ulx.url1.date then
    format = "none"
+
format = "none"
  else
+
end
    if format == "addlpages" then
+
elseif format == "addlarchives" then
      if not ulx.url1.date then
+
format = "addlarchives"
        format = "none"
+
else
      end
+
format = "none"
    elseif format == "addlarchives" then
+
end
      format = "addlarchives"
+
end
    else
+
ulx.url1.format = format
      format = "none"
 
    end
 
  end
 
  ulx.url1.format = format
 
  
                                                          -- Title argument  
+
if args.title and args.title1 then -- Title argument
 +
return inlineError (data.crit_err_msgs.conflicting, {origin.title, origin.title1});
 +
end
  
  local title = trimArg(args.title) or trimArg(args.title1)
+
ulx.url1.title = args.title or args.title1;
  ulx.url1.title = title
 
 
 
  
  local rend = createRendering()
+
local rend = createRendering()
  if not rend then
+
if not rend then
    rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
+
return inlineError (data.crit_err_msgs.unknown);
    track["Category:Webarchive template errors"] = 1
+
end
  end
 
  
  return rend .. createTracking()
+
return rend .. ((unnamed_params and inlineRed (err_warn_msgs.unnamed_params, 'warning')) or '') .. createTracking();
  
 
end
 
end
  
return p
+
 
 +
--[[--------------------------< E X P O R T E D F U N C T I O N S >------------------------------------------
 +
]]
 +
 
 +
return {webarchive = webarchive};

Latest revision as of 08:53, 17 May 2022

Module:Webarchive (talk⧼dot-separator⧽edit⧼dot-separator⧽hist⧼dot-separator⧽links⧼dot-separator⧽doc⧼dot-separator⧽subpages⧼dot-separator⧽tests (results)⧼dot-separator⧽sandbox (edit · subpages))

This module implements Template:webarchive (talk⧼dot-separator⧽links⧼dot-separator⧽edit).

Tracking categories


--[[ ----------------------------------

Lua module implementing the {{webarchive}} template. 

A merger of the functionality of three templates: {{wayback}}, {{webcite}} and {{cite archives}}
	
]]


--[[--------------------------< D E P E N D E N C I E S >------------------------------------------------------
]]

require('Module:No globals');
local getArgs = require ('Module:Arguments').getArgs;


--[[--------------------------< F O R W A R D   D E C L A R A T I O N S >--------------------------------------
]]

local categories = {};															-- category names
local config = {};																-- global configuration settings
local digits = {};																-- for i18n; table that translates local-wiki digits to western digits
local err_warn_msgs = {};														-- error and warning messages
local excepted_pages = {};
local month_num = {};															-- for i18n; table that translates local-wiki month names to western digits
local prefixes = {};															-- service provider tail string prefixes
local services = {};															-- archive service provider data from
local s_text = {};																-- table of static text strings used to build final rendering
local uncategorized_namespaces = {};											-- list of namespaces that we should not categorize
local uncategorized_subpages = {};												-- list of subpages that should not be categorized


--[[--------------------------< P A G E   S C O P E   I D E N T I F I E R S >----------------------------------
]]

local non_western_digits;														-- boolean flag set true when data.digits.enable is true
local this_page = mw.title.getCurrentTitle();

local track = {};																-- Associative array to hold tracking categories
local ulx = {};																	-- Associative array to hold template data 


--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------

Populates numbered arguments in a message string using an argument table.

]]

local function substitute (msg, args)
	return args and mw.message.newRawMessage (msg, args):plain() or msg;
end


--[[--------------------------< tableLength >-----------------------

Given a 1-D table, return number of elements

]]

local function tableLength(T)
	local count = 0
	for _ in pairs(T) do count = count + 1 end
	return count
end


--[=[-------------------------< M A K E _ W I K I L I N K >----------------------------------------------------

Makes a wikilink; when both link and display text is provided, returns a wikilink in the form [[L|D]]; if only
link is provided, returns a wikilink in the form [[L]]; if neither are provided or link is omitted, returns an
empty string.

]=]

local function make_wikilink (link, display, no_link)
	if nil == no_link then
		if link and ('' ~= link) then
			if display and ('' ~= display) then
				return table.concat ({'[[', link, '|', display, ']]'});
			else
				return table.concat ({'[[', link, ']]'});
			end
		end
		return display or '';													-- link not set so return the display text

	else																		-- no_link
		if display and ('' ~= display) then										-- if there is display text
			return display;														-- return that
		else
			return link or '';													-- return the target article name or empty string
		end
	end
end


--[[--------------------------< createTracking >-----------------------

Return data in track[] ie. tracking categories

]]

local function createTracking()
	if not excepted_pages[this_page.fullText] then								-- namespace:title/fragment is allowed to be categorized (typically this module's / template's testcases page(s))
		if uncategorized_namespaces[this_page.nsText] then
			return '';															-- this page not to be categorized so return empty string
		end
		for _,v in ipairs (uncategorized_subpages) do							-- cycle through page name patterns
			if this_page.text:match (v) then									-- test page name against each pattern
				return '';														-- this subpage type not to be categorized so return empty string
			end
		end
	end

	local out = {};
	if tableLength(track) > 0 then
		for key, _ in pairs(track) do											-- loop through table
			table.insert (out, make_wikilink (key));							-- and convert category names to links
		end
	end
	return table.concat (out);													-- concat into one big string; empty string if table is empty

end


--[[--------------------------< inlineError >-----------------------

Critical error. Render output completely in red. Add to tracking category.

This function called as the last thing before abandoning this module

]]

local function inlineError (msg, args)
	track[categories.error] = 1
	return table.concat ({
		'<span style="font-size:100%" class="error citation-comment">Error in ',	-- open the error message span
		config.tname,															-- insert the local language template name
		' template: ',
		substitute (msg, args),													-- insert the formatted error message
		'.</span>',																-- close the span
		createTracking()														-- add the category
		})
end


--[[--------------------------< inlineRed >-----------------------

Render a text fragment in red, such as a warning as part of the final output.
Add tracking category.

 ]]

local function inlineRed(msg, trackmsg)
	if trackmsg == "warning" then
		track[categories.warning] = 1;
	elseif trackmsg == "error" then
		track[categories.error] = 1;
	end

	return '<span style="font-size:100%" class="error citation-comment">' .. msg .. '</span>'
end


--[[--------------------------< base62 >-----------------------

Convert base-62 to base-10
Credit: https://de.wikipedia.org/wiki/Modul:Expr 

]]

local function base62( value )
	local r = 1																	-- default return value is input value is malformed

	if value:match ('%W') then													-- value must only be in the set [0-9a-zA-Z]
		return;																	-- nil return when value contains extraneous characters
	end

	local n = #value															-- number of characters in value
	local k = 1
	local c
	r = 0
	for i = n, 1, -1 do															-- loop through all characters in value from ls digit to ms digit
		c = value:byte( i, i )
		if c >= 48 and c <= 57 then												-- character is digit 0-9
			c = c - 48
		elseif c >= 65 and c <= 90 then											-- character is ascii a-z
			c = c - 55
		else																	-- must be ascii A-Z
			c = c - 61
		end
		r = r + c * k															-- accumulate this base62 character's value
		k = k * 62																-- bump for next
	end -- for i

	return r
end 


--[[--------------------------< D E C O D E _ D A T E >--------------------------------------------------------

Given a date string, return it in iso format along with an indicator of the date's format.  Except that month names
must be recognizable as legitimate month names with proper capitalization, and that the date string must match one
of the recognized date formats, no error checking is done here; return nil else

]]

local function decode_date (date_str)
	local patterns = {
		['dmy'] = {'^(%d%d?) +([^%s%d]+) +(%d%d%d%d)$', 'd', 'm', 'y'},			-- %a does not recognize unicode combining characters used by some languages
		['mdy'] = {'^([^%s%d]+) (%d%d?), +(%d%d%d%d)$', 'm', 'd', 'y'},
		['ymd'] = {'^(%d%d%d%d) +([^%s%d]+) (%d%d?)$', 'y', 'm', 'd'},			-- not mos compliant at en.wiki but may be acceptible at other wikis
		};
	
	local t = {};

	if non_western_digits then													-- this wiki uses non-western digits?
		date_str = mw.ustring.gsub (date_str, '%d', digits);					-- convert this wiki's non-western digits to western digits
	end

	if date_str:match ('^%d%d%d%d%-%d%d%-%d%d$') then							-- already an iso format date, return western digits form
		return date_str, 'iso';
	end
	
	for k, v in pairs (patterns) do
		local c1, c2, c3 = mw.ustring.match (date_str, patterns[k][1]);			-- c1 .. c3 are captured but we don't know what they hold
		
		if c1 then																-- set on match
			t = {																-- translate unspecified captures to y, m, and d
				[patterns[k][2]] = c1,											-- fill the table of captures with the captures
				[patterns[k][3]] = c2,											-- take index names from src_pattern table and assign sequential captures
				[patterns[k][4]] = c3,
				};
			if month_num[t.m] then												-- when month not already a number
				t.m = month_num[t.m];											-- replace valid month name with a number
			else
				return nil, 'iso';												-- not a valid date form because month not valid
			end

			return mw.ustring.format ('%.4d-%.2d-%.2d', t.y, t.m, t.d), k;		-- return date in iso format
		end
	end
	return nil, 'iso';															-- date could not be decoded; return nil and default iso date
end

	
--[[--------------------------< makeDate >-----------------------

Given year, month, day numbers, (zero-padded or not) return a full date in df format
where df may be one of:
	mdy, dmy, iso, ymd

on entry, year, month, day are presumed to be correct for the date that they represent; all are required

in this module, makeDate() is sometimes given an iso-format date in year:
	makeDate (2018-09-20, nil, nil, df)
this works because table.concat() sees only one table member

]]

local function makeDate (year, month, day, df)
	local format = {
		['dmy'] = 'j F Y',
		['mdy'] = 'F j, Y',
		['ymd'] = 'Y F j',
		['iso'] = 'Y-m-d',
		};

	local date = table.concat ({year, month, day}, '-');						-- assemble year-initial numeric-format date (zero padding not required here)

	if non_western_digits then													-- this wiki uses non-western digits?
		date = mw.ustring.gsub (date, '%d', digits);							-- convert this wiki's non-western digits to western digits
	end

	return mw.getContentLanguage():formatDate (format[df], date);
end


--[[--------------------------< I S _ V A L I D _ D A T E >----------------------------------------------------

Returns true if date is after 31 December 1899 (why is 1900 the min year? shouldn't the internet's date-of-birth
be min year?), not after today's date, and represents a valid date (29 February 2017 is not a valid date).  Applies
Gregorian leapyear rules.

all arguments are required

]]

local 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;
	local y, m, d;
	local today = os.date ('*t');												-- fetch a table of current date parts

	if not year or '' == year or not month or '' == month or not day or '' == day then
		return false;															-- something missing
	end
	
	y = tonumber (year);
	m = tonumber (month);
	d = tonumber (day);

	if 1900 > y or today.year < y or 1 > m or 12 < m then						-- year and month are within bounds	TODO: 1900?
		return false;
	end

	if (2==m) then																-- if February
		month_length = 28;														-- then 28 days unless
		if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then						-- is a leap year?
			month_length = 29;													-- if leap year then 29 days in February
		end
	else
		month_length=days_in_month[m];
	end

	if 1 > d or month_length < d then											-- day is within bounds
		return false;
	end
																					-- here when date parts represent a valid date
	return os.time({['year']=y, ['month']=m, ['day']=d, ['hour']=0}) <= os.time();	-- date at midnight must be less than or equal to current date/time
end


--[[--------------------------< decodeWebciteDate >-----------------------

Given a URI-path to Webcite (eg. /67xHmVFWP) return the encoded date in df format

returns date string in df format - webcite date is a unix timestamp encoded as bae62
or the string 'query'

]]

local function decodeWebciteDate(path, df)

	local dt = {};
	local decode;

	dt = mw.text.split(path, "/")

	-- valid URL formats that are not base62

	-- http://www.webcitation.org/query?id=1138911916587475
	-- http://www.webcitation.org/query?url=http..&date=2012-06-01+21:40:03
	-- http://www.webcitation.org/1138911916587475
	-- http://www.webcitation.org/cache/73e53dd1f16cf8c5da298418d2a6e452870cf50e
	-- http://www.webcitation.org/getfile.php?fileid=1c46e791d68e89e12d0c2532cc3cf629b8bc8c8e

	if dt[2]:find ('query', 1, true) or 
		dt[2]:find ('cache', 1, true) or
		dt[2]:find ('getfile', 1, true) or
		tonumber(dt[2]) then
			return 'query';
	end

	decode = base62(dt[2]);														-- base62 string -> exponential number
	if not decode then
		return nil;																-- nil return when dt[2] contains characters not in %w
	end
	dt = os.date('*t', string.format("%d", decode):sub(1,10))					-- exponential number -> text -> first 10 characters (a unix timestamp) -> a table of date parts

	decode = makeDate (dt.year, dt.month, dt.day, 'iso');						-- date comparisons are all done in iso format with western digits
	if non_western_digits then													-- this wiki uses non-western digits?
		decode = mw.ustring.gsub (decode, '%d', digits);						-- convert this wiki's non-western digits to western digits
	end

	return decode;
end


--[[--------------------------< decodeWaybackDate >-----------------------

Given a URI-path to Wayback (eg. /web/20160901010101/http://example.com )
or Library of Congress Web Archives (eg. /all/20160901010101/http://example.com)
or UK Government Web Archive (eg. /ukgwa/20160901010101/http://example.com or /tna/20160901010101/http://example.com)

return the formatted date eg. "September 1, 2016" in df format 
Handle non-digits in snapshot ID such as "re_" and "-" and "*"

returns two values:
	first value is one of these:
		valid date string in df format - wayback date is valid (including the text string 'index' when date is '/*/')
		empty string - wayback date is malformed (less than 8 digits, not a valid date)
		nil - wayback date is '/save/' or otherwise not a number
	
	second return value is an appropriate 'message' may or may not be formatted

]]

local function decodeWaybackDate(path, df)

	local msg, snapdate;

	snapdate = path:gsub ('^/web/', ''):gsub ('^/all/', ''):gsub ('^/ukgwa/', ''):gsub ('^/tna/', ''):gsub ('^/', '');	-- remove leading /web/, /all/, /ukgwa/, /tna/, or /
	snapdate = snapdate:match ('^[^/]+');										-- get timestamp
	if snapdate == "*" then														-- eg. /web/*/http.., etc.
		return 'index';															-- return indicator that this url has an index date
	end

	snapdate = snapdate:gsub ('%a%a_%d?$', ''):gsub ('%-', '');					-- from date, remove any trailing "re_", dashes

	msg = '';
	if snapdate:match ('%*$') then												-- a trailing '*' causes calendar display at archive .org
		snapdate = snapdate:gsub ('%*$', '');									-- remove so not part of length calc later
		msg = inlineRed (err_warn_msgs.ts_cal, 'warning');						-- make a message
	end

	if not tonumber(snapdate) then
		return nil, 'ts_nan';													-- return nil (fatal error flag) and message selector
	end

	local dlen = snapdate:len();
	if dlen < 8 then															-- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
		return '', inlineRed (err_warn_msgs.ts_short, 'error');					-- return empty string and error message
	end

	local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)');			-- no need for snapdatelong here

	if not is_valid_date (year, month, day) then
		return '', inlineRed (err_warn_msgs.ts_date, 'error');					-- return empty string and error message
	end

	snapdate = table.concat ({year, month, day}, '-');							-- date comparisons are all done in iso format
	if 14 == dlen then
		return snapdate, msg;													-- return date with message if any
	else
		return snapdate, msg .. inlineRed (err_warn_msgs.ts_len, 'warning');	-- return date with warning message(s)
	end
end


--[[--------------------------< decodeArchiveisDate >-----------------------

Given an Archive.is "long link" URI-path (e.g. /2016.08.28-144552/http://example.com)
return the date in df format (e.g. if df = dmy, return 28 August 2016)
Handles "." and "-" in snapshot date, so 2016.08.28-144552 is same as 20160828144552

returns two values:
	first value is one of these:
		valid date string in df format - archive.is date is valid (including the text string 'short link' when url is the short form)
		empty string - wayback date is malformed (not a number, less than 8 digits, not a valid date)
		nil - wayback date is '/save/'
	
	second return value is an appropriate 'message' may or may not be formatted

]]

local function decodeArchiveisDate(path, df)
	local snapdate

	if path:match ('^/%w+$') then												-- short form url path is '/' followed by some number of base 62 digits and nothing else
		return "short link"														-- e.g. http://archive.is/hD1qz
	end

	snapdate = mw.text.split (path, '/')[2]:gsub('[%.%-]', '');					-- get snapshot date, e.g. 2016.08.28-144552; remove periods and hyphens

	local dlen = string.len(snapdate)
	if dlen < 8 then															-- we need 8 digits TODO: but shouldn't this be testing for 14 digits?
		return '', inlineRed (err_warn_msgs.ts_short, 'error');					-- return empty string and error message
	end

	local year, month, day = snapdate:match ('(%d%d%d%d)(%d%d)(%d%d)');			-- no need for snapdatelong here

	if not is_valid_date (year, month, day) then
		return '', inlineRed (err_warn_msgs.ts_date, 'error');					-- return empty string and error message
	end

	snapdate = table.concat ({year, month, day}, '-');							-- date comparisons are all done in iso format
	if 14 == dlen then
		return snapdate;														-- return date
	else
		return snapdate, inlineRed (err_warn_msgs.ts_len, 'warning');			-- return date with warning message
	end
 end


--[[--------------------------< serviceName >-----------------------

Given a domain extracted by mw.uri.new() (eg. web.archive.org) set tail string and service ID

]]

local function serviceName(host, no_link)
	local tracking;
	local index;
	
	host = host:lower():gsub ('^web%.(.+)', '%1'):gsub ('^www%.(.+)', '%1');	-- lowercase, remove web. and www. subdomains

	if services[host] then
		index = host;
	else
		for k, _ in pairs (services) do
			if host:find ('%f[%a]'..k:gsub ('([%.%-])', '%%%1')) then
				index = k;
				break;
			end
		end
	end
	
	if index then
		local out = {''};														-- empty string in [1] so that concatenated result has leading single space
		ulx.url1.service = services[index][4] or 'other';
		tracking = services[index][5] or categories.other;
																				-- build tail string
		if false == services[index][1] then										-- select prefix
			table.insert (out, prefixes.at);
		elseif true == services[index][1] then
			table.insert (out, prefixes.atthe);
		else
			table.insert (out, services[index][1]);
		end
		
		table.insert (out, make_wikilink (services[index][2], services[index][3], no_link));	-- add article wikilink
		if services[index][6] then												-- add tail postfix if it exists
			table.insert (out, services[index][6]);
		end
		
		ulx.url1.tail = table.concat (out, ' ');								-- put it all together; result has leading space character

	else																		-- here when unknown archive
		ulx.url1.service = 'other';
		tracking = categories.unknown;
		ulx.url1.tail = table.concat ({'', prefixes.at, host, inlineRed (err_warn_msgs.unknown_url, error)}, ' ');
	end
	
	track[tracking] = 1
end


--[[--------------------------< parseExtraArgs >-----------------------

Parse numbered arguments starting at 2, such as url2..url10, date2..date10, title2..title10
	For example: {{webarchive |url=.. |url4=.. |url7=..}}
		Three url arguments not in numeric sequence (1..4..7). 
			Function only processes arguments numbered 2 or greater (in this case 4 and 7)
				It creates numeric sequenced table entries like:
				urlx.url2.url = <argument value for url4>
				urlx.url3.url = <argument value for url7>
			Returns the number of URL arguments found numbered 2 or greater (in this case returns "2")

 ]]

local function parseExtraArgs(args)

	local i, j, argurl, argurl2, argdate, argtitle

	j = 2
	for i = 2, config.maxurls do
		argurl = "url" .. i
		if args[argurl] then
			argurl2 = "url" .. j
			ulx[argurl2] = {}
			ulx[argurl2]["url"] = args[argurl]
			argdate = "date" .. j
			if args[argdate] then
				ulx[argurl2]["date"] = args[argdate]
			else
				ulx[argurl2]["date"] = inlineRed (err_warn_msgs.date_miss, 'warning');
			end
	
			argtitle = "title" .. j
			if args[argtitle] then
				ulx[argurl2]["title"] = args[argtitle]
			else
				ulx[argurl2]["title"] = nil
			end
			j = j + 1
		end
	end

	if j == 2 then
		return 0
	else
		return j - 2
	end
end


--[[--------------------------< comma >-----------------------

Given a date string, return "," if it's MDY 

]]

local function comma(date)
	return (date and date:match ('%a+ +%d%d?(,) +%d%d%d%d')) or '';
end


--[[--------------------------< createRendering >-----------------------

Return a rendering of the data in ulx[][]

]]

local function createRendering()

	local displayfield
	local out = {};
	
	local index_date, msg = ulx.url1.date:match ('(index)(.*)');				-- when ulx.url1.date extract 'index' text and message text (if there is a message)
	ulx.url1.date = ulx.url1.date:gsub ('index.*', 'index');					-- remove message

	if 'none' == ulx.url1.format then											-- For {{wayback}}, {{webcite}}
		table.insert (out, '[');												-- open extlink markup
		table.insert (out, ulx.url1.url);										-- add url

		if ulx.url1.title then
			table.insert (out, ' ')												-- the required space
			table.insert (out, ulx.url1.title)									-- the title
			table.insert (out, ']');											-- close extlink markup
			table.insert (out, ulx.url1.tail);									-- tail text
			if ulx.url1.date then
				table.insert (out, '&#32;(');									-- open date text; TODO: why the html entity? replace with regular space?
				table.insert (out, 'index' == ulx.url1.date and s_text.archive or s_text.archived);	-- add text
				table.insert (out, ' ');										-- insert a space
				table.insert (out, ulx.url1.date);								-- add date
				table.insert (out, ')');										-- close date text
			end
		else																	-- no title
			if index_date then													-- when url date is 'index' 
				table.insert (out, table.concat ({' ', s_text.Archive_index, ']'}));	-- add the index link label
				table.insert (out, msg or '');									-- add date mismatch message when url date is /*/ and |date= has valid date
			else
				table.insert (out, table.concat ({' ', s_text.Archived, '] '}));	-- add link label for url has timestamp date (will include mismatch message if there is one)
			end
			if ulx.url1.date then
				if 'index' ~= ulx.url1.date then
					table.insert (out, ulx.url1.date);							-- add date when data is not 'index'
				end
				table.insert (out, comma(ulx.url1.date));						-- add ',' if date format is mdy
				table.insert (out, ulx.url1.tail);								-- add tail text
			else																-- no date
				table.insert (out, ulx.url1.tail);								-- add tail text
			end
		end

		if 0 < ulx.url1.extraurls then											-- For multiple archive URLs
			local tot = ulx.url1.extraurls + 1
			table.insert (out, '.')												-- terminate first url
			table.insert (out, table.concat ({' ', s_text.addlarchives, ': '}));	-- add header text

			for i=2, tot do														-- loop through the additionals
				local index = table.concat ({'url', i});						-- make an index
				displayfield = ulx[index]['title'] and 'title' or 'date';		-- choose display text
				table.insert (out, '[');										-- open extlink markup
				table.insert (out, ulx[index]['url']);							-- add the url
				table.insert (out, ' ');										-- the required space
				table.insert (out, ulx[index][displayfield]);					-- add the label
				table.insert (out, ']');										-- close extlink markup
				table.insert (out, i==tot and '.' or ', ');						-- add terminator
			end
		end
		return table.concat (out);												-- make a big string and done

	else																		-- For {{cite archives}}																	
		if 'addlarchives' == ulx.url1.format then								-- Multiple archive services 
			table.insert (out, table.concat ({s_text.addlarchives, ': '}));		-- add header text
		else																	-- Multiple pages from the same archive 
			table.insert (out, table.concat ({s_text.addlpages, ' '}));			-- add header text
			table.insert (out, ulx.url1.date);									-- add date to header text
			table.insert (out, ': ');											-- close header text
		end

		local tot = ulx.url1.extraurls + 1;
		for i=1, tot do															-- loop through the additionals
			local index = table.concat ({'url', i});							-- make an index
			table.insert (out, '[');											-- open extlink markup
			table.insert (out, ulx[index]['url']);								-- add url
			table.insert (out, ' ');											-- add required space

			displayfield = ulx[index]['title'];
			if 'addlarchives' == ulx.url1.format then
				if not displayfield then 
					displayfield = ulx[index]['date']
				end
			else																-- must be addlpages
				if not displayfield then 
					displayfield = table.concat ({s_text.Page, ' ', i});
				end
			end
			table.insert (out, displayfield);									-- add title, date, page label text
			table.insert (out, ']');											-- close extlink markup
			table.insert (out, (i==tot and '.' or ', '));							-- add terminator
		end
		return table.concat (out);												-- make a big string and done
	end
end


--[[--------------------------< P A R A M E T E R _ N A M E _ X L A T E >--------------------------------------

for internaltionalization, translate local-language parameter names to their English equivalents

TODO: return error message if multiple aliases of the same canonical parameter name are found?

returns two tables:
	new_args - holds canonical form parameters and their values either from translation or because the parameter was already in canonical form
	origin - maps canonical-form parameter names to their untranslated (local language) form for error messaging in the local language

unrecognized parameters are ignored

]]

local function parameter_name_xlate (args, params, enum_params)
	local name;																	-- holds modifiable name of the parameter name during evaluation
	local enum;																	-- for enumerated parameters, holds the enumerator during evaluation
	local found = false;														-- flag used to break out of nested for loops
	local new_args = {};														-- a table that holds canonical and translated parameter k/v pairs
	local origin = {};															-- a table that maps original (local language) parameter names to their canonical name for local language error messaging
	local unnamed_params;														-- set true when unsupported positional parameters are detected
	
	for k, v in pairs (args) do													-- loop through all of the arguments in the args table
		name = k;																-- copy of original parameter name

		if 'string' == type (k) then
			if non_western_digits then											-- true when non-western digits supported at this wiki
				name = mw.ustring.gsub (name, '%d', digits);					-- convert this wiki's non-western digits to western digits
			end
			
			enum = name:match ('%d+$');											-- get parameter enumerator if it exists; nil else
			
			if not enum then													-- no enumerator so looking for non-enumnerated parameters
				-- TODO: insert shortcut here? if params[name] then name holds the canonical parameter name; no need to search further
				for pname, aliases in pairs (params) do							-- loop through each parameter the params table
					for _, alias in ipairs (aliases) do							-- loop through each alias in the parameter's aliases table
						if name == alias then
							new_args[pname] = v;								-- create a new entry in the new_args table
							origin [pname] = k;									-- create an entry to make canonical parameter name to original local language parameter name
							found = true;										-- flag so that we can break out of these nested for loops
							break;												-- no need to search the rest of the aliases table for name so go on to the next k, v pair
						end
					end
	
					if found then												-- true when we found an alias that matched name
						found = false;											-- reset the flag
						break;													-- go do next args k/v pair
					end
				end
			else																-- enumerated parameters
				name = name:gsub ('%d$', '#');									-- replace enumeration digits with place holder for table search
				-- TODO: insert shortcut here? if num_params[name] then name holds the canonical parameter name; no need to search further
				for pname, aliases in pairs (enum_params) do					-- loop through each parameter the num_params table
					for _, alias in ipairs (aliases) do							-- loop through each alias in the parameter's aliases table
						if name == alias then
							pname = pname:gsub ('#$', enum);					-- replace the '#' place holder with the actual enumerator
							new_args[pname] = v;								-- create a new entry in the new_args table
							origin [pname] = k;									-- create an entry to make canonical parameter name to original local language parameter name
							found = true;										-- flag so that we can break out of these nested for loops
							break;												-- no need to search the rest of the aliases table for name so go on to the next k, v pair
						end
					end
	
					if found then												-- true when we found an alias that matched name
						found = false;											-- reset the flag
						break;													-- go do next args k/v pair
					end
				end
			end
		else
			unnamed_params = true;												-- flag for unsupported positional parameters
		end
	end																			-- for k, v
	return new_args, origin, unnamed_params;
end


--[[--------------------------< W E B A R C H I V E >----------------------------------------------------------

template entry point

]]

local function webarchive(frame)
	local args = getArgs (frame);

	local data = mw.loadData (table.concat ({									-- make a data module name; sandbox or live
		'Module:Webarchive/data',
		frame:getTitle():find('sandbox', 1, true) and '/sandbox' or ''			-- this instance is ./sandbox then append /sandbox
		}));
	categories = data.categories;												-- fill in the forward declarations
	config = data.config;
	if data.digits.enable then
		digits = data.digits;													-- for i18n; table of digits in the local wiki's language
		non_western_digits = true;												-- use_non_western_digits
	end
	err_warn_msgs = data.err_warn_msgs;
	excepted_pages = data.excepted_pages;
	month_num = data.month_num;													-- for i18n; table of month names in the local wiki's language
	prefixes = data.prefixes;
	services = data.services;
	s_text = data.s_text;
	uncategorized_namespaces = data.uncategorized_namespaces;
	uncategorized_subpages = data.uncategorized_subpages;

	local origin = {};															-- holds a map of English to local language parameter names used in the current template; not currently used
	local unnamed_params;														-- boolean set to true when template call has unnamed parameters
	args, origin, unnamed_params = parameter_name_xlate (args, data.params, data.enum_params);	-- translate parameter names in args to English

	local date, format, msg, udate, uri, url;
	local ldf = 'iso';															-- when there is no |date= parameter, render url dates in iso format
	
	if args.url and args.url1 then												-- URL argument (first)
		return inlineError (data.crit_err_msgs.conflicting, {origin.url, origin.url1});
	end
	
	url = args.url or args.url1;
	
	if not url then
		return inlineError (data.crit_err_msgs.empty);
	end
																				-- these iabot bugs perportedly fixed; removing these causes lua script error
--[[																				-- at Template:Webarchive/testcases/Production; resolve that before deleting these tests
	if mw.ustring.find( url, "https://web.http", 1, true ) then					-- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
		track[categories.error] = 1;
		return inlineError (data.crit_err_msgs.iabot1);
	end 
	if url == "https://web.archive.org/http:/" then								 -- track bug - TODO: IAbot bug; not known if the bug has been fixed; deferred
		track[categories.error] = 1;
		return inlineError (data.crit_err_msgs.iabot2);
	end
]]

	if not (url:lower():find ('^http') or url:find ('^//')) then
		return inlineError (data.crit_err_msgs.invalid_url );
	end

	ulx.url1 = {}
	ulx.url1.url = url

	ulx.url1.extraurls = parseExtraArgs(args)

	local good = false;
	good, uri = pcall (mw.uri.new, ulx.url1.url);								-- get a table of uri parts from this url; protected mode to prevent lua error when ulx.url1.url is malformed
	
	if not good or nil == uri.host then											-- abandon when ulx.url1.url is malformed
		return inlineError (data.crit_err_msgs.invalid_url);
	end
	
	serviceName(uri.host, args.nolink)

	if args.date and args.date1 then											-- Date argument
		return inlineError (data.crit_err_msgs.conflicting, {origin.date, origin.date1});
	end
	
	date = args.date or args.date1;
	date = date and date:gsub (' +', ' ');										-- replace multiple spaces with a single space

	if date and config.verifydates then
		if '*' == date then
			date = 'index';
			ldf = 'iso';														-- set to default format
		elseif 'mdy' == date then
			date = nil;															-- if date extracted from URL,
			ldf = 'mdy';														-- then |date=mdy overrides iso
		elseif 'dmy' == date then
			date = nil;															-- if date extracted from URL,
			ldf = 'dmy';														-- then |date=dmy overrides iso
		elseif 'ymd' == date then
			date = nil;															-- if date extracted from URL,
			ldf = 'ymd';														-- then |date=ymd overrides iso
		else
			date, ldf = decode_date (date);										-- get an iso format date from date and get date's original format
		end
	end

	if 'wayback' == ulx.url1.service or 'locwebarchives' == ulx.url1.service or 'ukgwa' == ulx.url1.service then
		if date then
			if config.verifydates then
				if ldf then
					udate, msg = decodeWaybackDate (uri.path);					-- get the url date in iso format and format of date in |date=; 'index' when wayback url date is *
					if not udate then											-- this is the only 'fatal' error return
						return inlineError (data.crit_err_msgs[msg]);
					end

					if udate ~= date then										-- date comparison using iso format dates
						date = udate;
						msg = table.concat ({
							inlineRed (err_warn_msgs.mismatch, 'warning'),		-- add warning message
							msg,												-- add message if there is one
						});
					end
				end
			end
		else																	-- no |date=
			udate, msg = decodeWaybackDate (uri.path);

			if not udate then													-- this is the only 'fatal' error return
				return inlineError (data.crit_err_msgs[msg]);
			end

			if '' == udate then 
				date = nil;														-- unset
			else
				date = udate;
			end
		end

	elseif 'webcite' == ulx.url1.service then
		if date then
			if config.verifydates then
				if ldf then
					udate = decodeWebciteDate (uri.path);						-- get the url date in iso format
					if 'query' ~= udate then									-- skip if query
						if udate ~= date then									-- date comparison using iso format dates
							date = udate;
							msg = table.concat ({
								inlineRed (err_warn_msgs.mismatch, 'warning'),
								});
						end
					end
				end
			end
		else
			date = decodeWebciteDate( uri.path, "iso" )
			if date == "query" then
				date = nil;														-- unset
				msg = inlineRed (err_warn_msgs.date_miss, 'warning');
			elseif not date then												-- invalid base62 string
				date = inlineRed (err_warn_msgs.date1, 'error');
			end
		end

	elseif 'archiveis' == ulx.url1.service then
		if date then
			if config.verifydates then
				if ldf then
					udate, msg = decodeArchiveisDate (uri.path)					-- get the url date in iso format
					if 'short link' ~= udate then								-- skip if short link
						if udate ~= date then									-- date comparison using iso format dates
							date = udate;
							msg = table.concat ({
								inlineRed (err_warn_msgs.mismatch, 'warning'),	-- add warning message
								msg,											-- add message if there is one
							});
						end
					end
				end
			end
		else																	-- no |date=
			udate, msg = decodeArchiveisDate( uri.path, "iso" )
			if udate == "short link" then
				date = nil;														-- unset
				msg = inlineRed (err_warn_msgs.date_miss, 'warning');
			elseif '' == udate then
				date = nil;														-- unset
			else
				date = udate;
			end
		end
		
	else																		-- some other service
		if not date then
			msg = inlineRed (err_warn_msgs.date_miss, 'warning');
		end
	end

	if 'index' == date then
		ulx.url1.date = date .. (msg or '');									-- create index + message (if there is one)
	elseif date then
		ulx.url1.date = makeDate (date, nil, nil, ldf) .. (msg or '');			-- create a date in the wiki's local language + message (if there is one)
	else
		ulx.url1.date = msg;
	end
		
	format = args.format;														-- Format argument 

	if not format then
		format = "none"
	else
		for k, v in pairs (data.format_vals) do									-- |format= accepts two specific values loop through a table of those values
			local found;														-- declare a nil flag
			for _, p in ipairs (v) do											-- loop through local language variants
				if format == p then												-- when |format= value matches 
					format = k;													-- use name from table key
					found = true;												-- declare found so that we can break out of outer for loop
					break;														-- break out of inner for loop
				end
			end
			
			if found then
				break;
			end
		end

		if format == "addlpages" then
			if not ulx.url1.date then
				format = "none"
			end
		elseif format == "addlarchives" then
			format = "addlarchives"
		else
			format = "none"
		end
	end
	ulx.url1.format = format

	if args.title and args.title1 then											-- Title argument
		return inlineError (data.crit_err_msgs.conflicting, {origin.title, origin.title1});
	end

	ulx.url1.title = args.title or args.title1;

	local rend = createRendering()
	if not rend then
		return inlineError (data.crit_err_msgs.unknown);
	end

	return rend .. ((unnamed_params and inlineRed (err_warn_msgs.unnamed_params, 'warning')) or '') .. createTracking();

end


--[[--------------------------< E X P O R T E D 	 F U N C T I O N S >------------------------------------------
]]

return {webarchive = webarchive};