local p = {}  
--==========================================================================
-- Globals
--==========================================================================  
local currtitle = mw.title.getCurrentTitle
local nexistingcats = 0
local errors = ''
local testcasecolon = ''
local testcases = string.match(currtitle.subpageText, '^testcases')
if testcases then testcasecolon = ':' end
local navborder = true
local followRs = true
local skipgaps = false
local listall = false
local tlistall = {}
local tlistallbwd = {}
local tlistallfwd = {}
local ttrackingcats = { --when reindexing, Ctrl+H 'trackcat(13,' & 'ttrackingcats[16]'
'', -- [1] placeholder for
'', -- [2] placeholder for
'', -- [3] placeholder for
'', -- [4] placeholder for
'', -- [5] placeholder for
'', -- [6] placeholder for
'', -- [7] placeholder for --new
'', -- [8] placeholder for
'', -- [9] placeholder for
'', --[10] placeholder for
'', --[11] placeholder for
'', --[12] placeholder for
'', --[13] placeholder for
'', --[14] placeholder for
'', --[15] placeholder for
'', --[16] placeholder for
'', --[17] placeholder for
'', --[18] placeholder for
'', --[19] placeholder for
'', --[20] placeholder for
'', --[21] placeholder for
'', --[22] placeholder for
'', --[23] placeholder for
'', --[24] placeholder for
'', --[25] placeholder for
'', --[26] placeholder for
'', --[27] placeholder for
'', --[28] placeholder for
'', --[29] placeholder for
'', --[30] placeholder for
'', --[31] placeholder for
}
local avoidself = (not string.match(currtitle.text, 'Navseasoncats with') and
not string.match(currtitle.text, 'Navseasoncats.*/doc') and
not string.match(currtitle.text, 'Navseasoncats.*/sandbox') and
currtitle.text ~= 'Navseasoncats' and
currtitle.nsText ~= 'User_talk' and
currtitle.nsText ~= 'Template_talk' and
(currtitle.nsText ~= 'Template' or testcases)) --avoid nested transclusion errors (i.e. )  
--==========================================================================
-- Utility & category functions
--==========================================================================  
--Determine if a category exists (in a function for easier localization).
local function catexists( title )
return mw.title.new( title, 'Category' ).exists
end  
--Error message handling.
function p.errorclass( msg )
return mw.text.tag( 'span', {}, '<b>Error!</b> '..string.gsub(msg, '&#', '&amp;#') )
end  
--Failure handling.
function p.failedcat( errors, sortkey )
if avoidself then
return (errors or '')..'&#42;&#42;&#42;Navseasoncats failed to generate navbox***'..
''..(sortkey or 'O')..'\n'
end
return ''
end  
--Tracking cat handling.
-- key: 15 (when reindexing ttrackingcats{}, Ctrl+H 'trackcat(13,' & 'ttrackingcats[16]')
-- cat: 'Navseasoncats isolated'; '' to remove
--Used by main, all nav_*, & several utility functions.
local function trackcat( key, cat )
if avoidself and key and cat then
if cat ~= '' then
ttrackingcats[key] = *..testcasecolon..'Category:'..cat..*
else
ttrackingcats[key] = ''
end
end
return
end  
--Check for unknown parameters.
--Used by main only.
local function checkforunknownparams( tbl )
local knownparams = {
['min'] = 'min',
['max'] = 'max',
['cat'] = 'cat',
['testcase'] = 'testcase',
['testcasegap'] = 'testcasegap',
['skip-gaps'] = 'skip-gaps',
['list-all-links'] = 'list-all-links',
['follow-redirects'] = 'follow-redirects',
}
for k, _ in pairs (tbl) do
if knownparams[k] == nil then
trackcat(3, 'Navseasoncats using unknown parameter')
break
end
end
end  
--Check for nav_* navigational isolation (not necessarily an error).
--Used by all nav_*.
local function isolatedcat
if nexistingcats == 0 then
trackcat(16, 'Navseasoncats isolated')
end
end  
--Returns the target of , if it exists, else returns the original cat.
--, etc., if found, are evaluated.
--Used by catlinkfollowr, and so indirectly by all nav_*.
local function rtarget( , cat )
local catcontent = mw.title.new( cat or '', 'Category' ):getContent
if string.match( catcontent or '', '
--Similar to : make a piped link to a category, if it exists;
--if it doesn't exist, just display the greyed link title without linking.
--Follows s.
--Returns {
-- ['cat'] = cat,
-- ['catexists'] = true,
-- ['rtarget'] = <#R target>,
-- ['navelement'] = <#R target navelement>,
-- ['displaytext'] = displaytext,
-- } if #R followed;
--returns {
-- ['cat'] = cat,
-- ['catexists'] = <true|false>,
-- ['rtarget'] = nil,
-- ['navelement'] = <cat navelement>,
-- ['displaytext'] = displaytext,
-- } otherwise.
--Used by all nav_*.
local function catlinkfollowr( , cat, displaytext, displayend )
cat = mw.text.trim(cat or '')
displaytext = mw.text.trim(displaytext or '')
displayend = displayend or false --bool flag to override displaytext IIF the cat/target is terminal (e.g. "2021–present" or "2021–")  
local grey = '#888'
local disp = cat
if displaytext ~= '' then --use 'displaytext' parameter if present
disp = mw.ustring.gsub(displaytext, '%s+%(.+$', ''); --strip any trailing disambiguator
end  
local link, nilorR
local exists = catexists(cat)
if exists then
nexistingcats = nexistingcats + 1
if followRs then
local R = rtarget(, cat) --find & follow #R
if R ~= cat then --#R followed
nilorR = R
end  
if displayend then
local y, hyph, ending = mw.ustring.match(R, '^.-(%d+)([–-])(.*)$')
if ending == 'present' then
disp = y..hyph..ending
elseif ending == '' then
disp = y..hyph..'<span>'..y..'</span>' --hidden y to match spacing
end
end  
link = *..disp..*
else
link = *..disp..*
end
else
link = '<span>'..disp..'</span>'
end  
if listall then
if nilorR then --#R followed
table.insert( tlistall, ':Category:'..cat..' → '..':Category:'..nilorR..' ('..link..')' )
else --no #R
table.insert( tlistall, ':Category:'..cat..' ('..link..')' )
end
end  
return {
['cat'] = cat,
['catexists'] = exists,
['rtarget'] = nilorR,
['navelement'] = link,
['displaytext'] = disp,
}
end  
--Returns a numbered list of all s followed by catlinkfollowr -> rtarget.
--For a nav_hyphen cat, also returns a formatted list of all cats searched for & found, & all loop indices.
--Used by all nav_*.
local function listalllinks
local nl = '\n# '
local out = ''
if currtitle.nsText == 'Category' then
errors = p.errorclass('The <b><code>|list-all-links=yes</code></b> parameter/utility '..
'should not be saved in category space, only previewed.')
out = p.failedcat(errors, 'Z')
end  
local bwd, fwd = *, *
if tlistallbwd[1] then
bwd = '\n\nbackward search:\n# '..table.concat(tlistallbwd, nl)
end
if tlistallfwd[1] then
fwd = '\n\nforward search:\n# '..table.concat(tlistallfwd, nl)
end  
if tlistall[1] then
return out..nl..table.concat(tlistall, nl)..bwd..fwd
else
return out..nl..'No links found!?'..bwd..fwd
end
end  
--Returns the difference b/w 2 ints separated by endash|hyphen, nil if error.
--Used by nav_hyphen only.
local function find_duration( cat )
local from, to = mw.ustring.match(cat, '(%d+)[–-](%d+)')
if from and to then
if to == '00' then return nil end --doesn't follow MOS:DATERANGE
if (#from == 4) and (#to == 2) then --1900-01
to = string.match(from, '(%d%d)%d%d')..to --1900-1901
elseif (#from == 2) and (#to == 4) then -- 01-1902
from = string.match(to, '(%d%d)%d%d')..from --1901-1902
end
return (tonumber(to) - tonumber(from))
end
return 0
end  
--Returns the ending of a terminal cat, and sets the appropriate tracking cat, else nil.
--Used by nav_hyphen only.
local function find_terminaltxt( cat )
local terminaltxt = nil
if mw.ustring.match(cat, '%d+[–-]present$') then
terminaltxt = 'present'
trackcat(14, 'Navseasoncats range ends (present)')
elseif mw.ustring.match(cat, '%d+[–-]$') then
terminaltxt = ''
trackcat(15, 'Navseasoncats range ends (blank, MOS)')
end
return terminaltxt
end  
--Returns an unsigned string of the 1-4 digit decade ending in "0", else nil.
--Used by nav_decade only.
local function sterilizedec( decade )
if decade == nil or decade == '' then
return nil
end  
local dec = string.match(decade, '^[-%+]?(%d?%d?%d?0)$') or
string.match(decade, '^[-%+]?(%d?%d?%d?0)%D')