# Module:Autotaxobox

--*************************************************************************  
This module provides support to the automated taxobox system – the templates  
Automatic taxobox, Speciesbox, Subspeciesbox, Infraspeciesbox, etc.  

In particular it provides a way of traversing the taxonomic hierarchy encoded  
in taxonomy templates (templates with names of the form  
"Template:Taxonomy/TAXON_NAME") without causing template expansion depth  
errors.  
                                                                                                                                                        - 

require('strict')  
local TaxonItalics = require('Module:TaxonItalics') -- use a function from Module:TaxonItalics to italicize a taxon name  
local TableRow = '|-\n'  
local TableEnd = '|}\n'  
local p = {} -- functions made public  
local l = {} -- internal functions, kept separate  
local colour = '' -- colour for taxobox and taxonomy listings  

--=========================================================================  
Limit the maximum depth of a taxonomic hierarchy that can be traversed;  
avoids excessive processing time and protects against incorrectly set up  
hierarchies, e.g. loops.  
The value can be obtained externally via  

=============================================================================  
local MaxSearchLevels = 100  

function p.getMaxSearchLevels  
return MaxSearchLevels  
end  

--========================== taxoboxColour ================================  
Determines the correct colour for a taxobox, by searching up the taxonomic  
hierarchy from the supplied taxon for the first taxon (other than  
'incertae sedis') that sets a taxobox colour. It is assumed that a valid  
taxobox colour is defined using CSS rgb syntax.  
If no taxon that sets a taxobox colour is found, then 'transparent' is  
returned unless the taxonomic hierarchy is too deep, when the error colour is  
returned.  
Usage:  
=============================================================================  
function p.taxoboxColour  
return p.getTaxoboxColour(, .args[1] or '')  
end  

function p.getTaxoboxColour(, currTaxon)  
-- note that colour is global to this function; default is empty string  
local i = 1 -- count levels processed  
local searching = currTaxon ~= '' -- still searching for a colour?  
local foundICTaxon = false -- record whether 'incertae sedis' found  
while searching and i <= MaxSearchLevels do  
local plainCurrTaxon, dummy = l.stripExtra(currTaxon) -- remove trailing text after /  
if string.lower(plainCurrTaxon) == 'incertae sedis' then  
foundICTaxon = true  
else  
local possibleColour = :expandTemplate{ title = 'Template:Taxobox colour', args = { plainCurrTaxon } }  
if string.sub(possibleColour,1,3) == 'rgb' then  
colour = possibleColour  
searching = false  
end  
end  
if searching then  
local ok, parent = p.getTaxonInfoItem(, currTaxon, 'parent')  
if ok and parent ~= '' then  
currTaxon = parent  
i = i + 1  
else  
searching = false -- run off the top of the hierarchy or tried to use non-existent taxonomy template  
end  
end  
end  
if colour == '' then  
if foundICTaxon then  
colour = :expandTemplate{ title = 'Template:Taxobox colour', args = { 'incertae sedis' } }  
elseif searching then  
-- hierarchy exceeds MaxSearchLevels levels  
colour = :expandTemplate{ title = 'Template:Taxobox/Error colour', args = { } }  
else  
colour = 'transparent'  
end  
end  
return colour  
end  

--= = = = = = = = = = = = = topLevelTaxon = = = = = = = = = = = = = = = =  
Defines the correct top level taxa, one of which should terminate every  
taxonomic hierarchy encoded in taxonomy templates.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.topLevelTaxon(taxon)  
return taxon == 'Life' or taxon == 'Veterovata' or taxon == 'Ichnos'  
end  

--=========================== taxoboxList =================================  
Returns the rows of taxa in an automated taxobox, based on the taxonomic  
hierarchy for the supplied taxon.  
Usage:  

=============================================================================  
function p.taxoboxList  
local currTaxon = .args[1] or ''  
if currTaxon == * then return * end  
local displayN = (tonumber(.args['display_taxa']) or 1) + 1  
local authTable = {}  
authTable[1] = .args['authority'] or ''  
authTable[2] = .args['parent_authority'] or ''  
authTable[3] = .args['gparent_authority'] or ''  
authTable[4] = .args['ggparent_authority'] or ''  
authTable[5] = .args['gggparent_authority'] or ''  
local boldFirst = .args['bold_first'] or 'link' -- values 'link' or 'bold'  
local virus = .args['virus'] or 'no' -- values 'yes' or 'no'  
local offset = tonumber(.args['offset'] or 0)  
-- adjust the authority table if 'authority' refers to a rank lower than the target taxon  
if offset ~= 0 then  
for i = 1, 5 do  
local j = i + offset  
if j <= 5 then  
authTable[i] = authTable[j]  
else  
authTable[i] = ''  
end  
end  
end  
local taxonTable, taxonRankTable = l.makeTable(, currTaxon)  
local res = ''  
local topTaxonN = taxonTable.n  
-- display all taxa above possible greatgreatgrandparent, without authority  
for i = topTaxonN, 6, -1 do  
res = res .. l.showTaxon(, taxonTable[i], taxonRankTable[i], topTaxonN==i, *, displayN >= i, *, virus)  
end  
-- display all taxa above possible parent, with authority if given  
for i = math.min(topTaxonN, 5), 2, -1 do  
res = res .. l.showTaxon(, taxonTable[i], taxonRankTable[i], topTaxonN==i, authTable[i], displayN >= i, '', virus)  
end  
-- display target taxon, always displayed and emboldened  
res = res .. l.showTaxon(, taxonTable[1], taxonRankTable[1], topTaxonN==1, authTable[1], true, boldFirst, virus)  
return res  
end  

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =  
Show one taxon row in a taxobox.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.showTaxon(, taxon, rank, isTopTaxon, auth, force, boldFirst, virus)  
-- it's an error if this is the top taxon and it's not a top level taxon (e.g. "Life")  
if isTopTaxon then  
if l.topLevelTaxon(taxon) then  
return '' -- don't display a top level taxon  
elseif (mw.title.new('Taxonomy/'..taxon, 'Template') or {}).exists then  
-- taxonomy template for this taxon has no parent specified  
return :expandTemplate{ title = 'Template:Create taxonomy', args = {taxon, msg='Taxonomy template does not specify a parent'} } .. '\n' .. TableRow  
else  
-- no taxonomy template for this taxon  
return :expandTemplate{ title = 'Template:Create taxonomy', args = {taxon, msg='Missing taxonomy template'} } .. '\n' .. TableRow  
end  
else  
-- if showing is not already forced, force if it's a principal rank or an authority is specified  
force = force or :expandTemplate{ title = 'Template:Principal rank', args = {rank} } == 'yes' or  
auth ~= ''  
if not force then  
-- if showing is still not already forced, force if the taxonomy template has 'always_display' set  
local ok, alwaysDisplay = p.getTaxonInfoItem(, taxon, 'always_display')  
force = alwaysDisplay == 'yes' or alwaysDisplay == 'true'  
end  
if force then  
local res = l.tableCell(:expandTemplate{ title = 'Template:Anglicise rank', args = {rank} } .. ':')  
local bold = 'no'  
if boldFirst == 'bold' then bold = 'yes' end  
if auth ~= '' then  
auth = '  
<small>' .. auth .. '</small>'  
end  
local res = res .. l.tableCell(l.getTaxonLink(, taxon, rank, bold, *, *, virus) .. auth) -- italic, abbreviated  
return res .. TableRow  
else  
return ''  
end  
end  
end  

--========================== taxonomyList =================================  
Returns the cells of the taxonomy table displayed on the hand side of  
"Template:Taxonomy...." pages.  
Usage:  
=============================================================================  
function p.taxonomyList  
local currTaxon = .args[1] or ''  
if currTaxon == '' then  
return '{|\n' .. TableRow .. l.tableCell('') .. l.tableCell('ERROR: no taxon supplied') .. TableEnd  
end  
local taxonTable, taxonRankTable = l.makeTable(, currTaxon)  
local rankValTable = l.getRankTable  
local lastRankVal = 1000000  
local orderOk = true  
-- check whether the taxonomy is for viruses; use already determined taxon colour if possible  
local virus = 'no'  
local taxoColour = colour  
if taxoColour == '' then  
if taxonTable[taxonTable.n] == 'Ichnos' or taxonTable[taxonTable.n] == 'Veterovata' then  
taxoColour = :expandTemplate{ title = 'Template:Taxobox colour', args = { taxonTable[taxonTable.n] } }  
else  
taxoColour = :expandTemplate{ title = 'Template:Taxobox colour', args = { taxonTable[taxonTable.n - 1] } }  
end  
end  
if taxoColour == :expandTemplate{ title = 'Template:Taxobox colour', args = { 'virus' } } then  
virus = 'yes'  
end  
-- add information message  
local res = '<p>Bold ranks show taxa that will be shown in taxoboxes  
because rank is principal or <code>always_display=yes</code>.</p>\n'  

-- start table  
res = res .. '{|\n' .. TableRow .. '! colspan=4|Ancestral taxa\n'  
-- deal first with the top level taxon; if there are no errors, it should be Life/Veterovata/Ichnos, which are  
-- not displayed  
local taxon = taxonTable[taxonTable.n]  
if not l.topLevelTaxon(taxon) then  
local msg = 'Taxonomy template missing'  
if mw.title.new('Taxonomy/'..taxon, 'Template').exists then  
msg = 'Parent taxon needed'  
end  
res = res .. TableRow .. l.tableCell('colspan=2', :expandTemplate{title = 'Template:Create taxonomy', args = {taxon, msg = msg}})  
end  
-- now output the rest of the table  
local currRankVal  
for i = taxonTable.n-1, 1, -1 do  
-- check ranks are in order in the hierarchy  
taxon = taxonTable[i]  
local rank = taxonRankTable[i]  
currRankVal = l.lookupRankVal(rankValTable, rank)  
if currRankVal then  
orderOk = currRankVal < lastRankVal  
if orderOk then lastRankVal = currRankVal end  
else  
orderOk = true  
end  
-- see if the row will be displayed in a taxobox; bold the rank if so  
local boldRank = false  
local ok, alwaysDisplay = p.getTaxonInfoItem(, taxon, 'always_display')  
if ok and (alwaysDisplay == 'yes' or alwaysDisplay == 'true') then  
boldRank = true  
else  
boldRank = :expandTemplate{ title = 'Template:Principal rank', args = {rank} } == 'yes'  
end  
-- now return a row of the taxonomy table with anomalous ranks marked  
local errorStr = ''  
if not orderOk then errorStr = 'yes' end  
local link = l.getTaxonLink(, taxon, rank, *, *, '', virus) -- bold, italic, abbreviated  
res = res .. l.taxonomyListRow(, taxon, rank, link, boldRank, errorStr)  
end  
-- close table  
res = res .. TableEnd  
-- error-tracking for taxonomy templates  
-- if the last row has an anomalous rank, put the page in an error-tracking category  
local errCat1 = ''  
if not orderOk then  
errCat1 = '\n'  
end  
-- if the last row has a taxon name in the page name that does not match the link text,  
-- put the taxonomy template in a tracking category  
local dummy, linkText = p.getTaxonInfoItem(, taxon, 'link_text')  
local match = l.matchTaxonLink(taxon, linkText, currRankVal and currRankVal < rankValTable['genus'])  
local errCat2 = ''  
if not match then  
errCat2 = '\n'  
end  
if errCat1..errCat2 ~= '' then  
res = res .. :expandTemplate{ title = 'Template other', args = { errCat1..errCat2} }  
end  
return res  
end  

-- = = = = = = = = = = = = = = taxonomyListRow = = = = = = = = = = = = = =  
Returns a single row of the taxonomy table displayed on the hand side  
of "Template:Taxonomy...." pages.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.taxonomyListRow(, taxon, rank, link, boldRank, error)  
local res = ''  
if taxon == * or rank == * then return res end  
local baseTaxon, qualifier = l.stripExtra(taxon)  
-- if appropriate, make it clear that some taxa have been skipped via a ... row  
if qualifier == '/skip' then  
res = res .. TableRow .. l.tableCell('.....') .. l.tableCell('.....')  
end  
-- now generate a row of the table  
res = res .. TableRow  
local cellContent = ''  
local anglicizedRank = :expandTemplate{ title = 'Template:Anglicise rank', args = { rank } }  
if boldRank then  
cellContent = cellContent .. '<b>' .. anglicizedRank .. '</b>:'  
else  
cellContent = cellContent .. anglicizedRank .. ':'  
end  
if error == 'yes' then  
cellContent = '<span>' .. cellContent .. '</span>'  
end  
res = res .. l.tableCell(cellContent)  
.. l.tableCell('<span>' .. link .. '</span>')  
.. l.tableCell('<span>' .. qualifier .. '</span>')  
.. l.tableCell('<span>' .. :expandTemplate{ title = 'Template:Edit a taxon', args = { taxon } } .. '</span>')  
return res  
end  

--========================= callTaxonomyKey ===============================  
Prepares for, and then calls, Template:Taxonomy key to display a taxonomy  
template page. It does this by building up the information the template  
requires, following one 'same as' link, if required.  
Usage:  

=============================================================================  
local PARENT = 1  
local RANK = 2  
local LINK_TARGET = 3  
local LINK_TEXT = 4  
local ALWAYS_DISPLAY = 5  
local EXTINCT = 6  
local SAME_AS = 7  
local REFS = 8  

function p.callTaxonomyKey  
local taxon = .args['taxon'] or ''  
local parent = .args['parent'] or ''  
local rank = .args['rank'] or ''  
local extinct = string.lower(.args['extinct']) or ''  
local alwaysDisplay = string.lower(.args['always_display']) or ''  
local linkTarget = .args['link_target'] or ''  
local linkText = .args['link_text'] or * -- this is the "raw" link text, and can be *  
local refs = .args['refs'] or ''  
local sameAsTaxon = .args['same_as'] or ''  
if sameAsTaxon ~= '' then  
-- try using the 'same as' taxon; it's an error if it doesn't exist  
local ok, sameAsInfoStr = pcall(.expandTemplate, , { title = 'Template:Taxonomy/' .. sameAsTaxon, args = {['machine code'] = 'all' } })  
if ok then  
local sameAsInfo = mw.text.split(sameAsInfoStr, '$', true)  
--'same as' taxon's taxonomy template must not have a 'same as' link  
if sameAsInfo[SAME_AS] == '' then  
if parent == '' then parent = sameAsInfo[PARENT] end  
if rank == '' then rank = sameAsInfo[RANK] end  
if extinct == '' then extinct = string.lower(sameAsInfo[EXTINCT]) end  
if alwaysDisplay == '' then alwaysDisplay = string.lower(sameAsInfo[ALWAYS_DISPLAY]) end  
if linkTarget == '' then linkTarget = sameAsInfo[LINK_TARGET] end  
if linkText == '' then linkText = sameAsInfo[LINK_TEXT] end  
if refs == '' and parent == sameAsInfo[PARENT] then refs = sameAsInfo[REFS] end  
else  
return '<span>Error: attempt to follow two "same as" links</span>: <code>same_as = ' .. sameAsTaxon .. '</code>, but Template:Taxonomy/' .. sameAsTaxon .. ' also has a<code>same_as</code> parameter.'  
end  
else  
return :expandTemplate{ title = 'Template:Taxonomy key/missing template', args = {taxon=sameAsTaxon, msg='given as the value of <code>same as</code>'} }  
end  
end  
local link = linkTarget  
if linkText ~= '' and linkText ~= linkTarget then link = link .. "|" .. linkText end  
-- check consistency of extinct status; if this taxon is not extinct, parent must not be either  
local extinctError = 'no'  
if parent ~= * and (extinct == * or extinct == 'no' or extinct == 'false') then  
local ok, parentExtinct = p.getTaxonInfoItem(, parent, 'extinct')  
if ok and (parentExtinct == 'yes' or parentExtinct == 'true') then extinctError = 'yes' end  
end  
return :expandTemplate{ title = 'Template:Taxonomy key',  
args = {taxon=taxon, parent=parent, rank=rank, extinct=extinct, always_display=alwaysDisplay, link_target=linkTarget, link=link, refs=refs, same_as=sameAsTaxon, extinct_error = extinctError} }  
end  

--============================= showRefs ==================================  
Shows the refs field in a taxonomy template, handing incertae sedis taxa and  
using '–' for absent refs.  
Usage:  
=============================================================================  
function p.showRefs  
local taxonName = .args[1] or ''  
local refs = .args[2] or ''  
return l.doShowRefs(taxonName, refs)  
end  

--= = = = = = = = = = = = = = doShowRefs = = = = = = = = = = = = = = = = =  
Show the refs field in a taxonomy template.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.doShowRefs(taxonName, refs)  
if mw.text.split(taxonName, '/', true)[1] == 'Incertae sedis' then  
refs = 'not applicable (<i>incertae sedis</i>)'  
elseif refs == '' then  
refs = '–'  
end  
return refs  
end  

--============================ taxonInfo ==================================  
Extracts and returns information from Template:Taxonomy/TAXON, following  
one 'same as' link if required.  
Usage:  
ITEM is one of: 'parent', 'rank', 'link target', 'link text', 'extinct',  
'always display', 'refs', 'same as' or 'all'.  
If ITEM is not specified, the default is 'all' – all values in a single string  
separated by '$'.  
=============================================================================  
function p.taxonInfo  
local taxon = .args[1] or ''  
local item = .args[2] or ''  
if item == '' then item = 'all' end  
local ok, info = p.getTaxonInfoItem(, taxon, item)  
return info  
end  

--= = = = = = = = = = = getTaxonInfoItem = = = = = = = = = = = = = = = = =  
Utility function to extract an item of information from a  
taxonomy template, following one 'same as' link if required.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function p.getTaxonInfoItem(, taxon, item)  
local ok, info  
-- item == 'dagger' is a special case  
if item == 'dagger' then  
ok, info = p.getTaxonInfoItem(, taxon, 'extinct')  
if ok then  
if info == 'yes' or info == 'true' then  
info = '&dagger;'  
else  
info = ''  
end  
end  
-- item ~= 'dagger'  
else  
ok, info = pcall(.expandTemplate, , { title = 'Template:Taxonomy/' .. taxon, args = {['machine code'] = item } })  
if ok then  
if info == '' then  
-- try 'same as'  
local sameAsTaxon = :expandTemplate{ title = 'Template:Taxonomy/' .. taxon, args = {['machine code'] = 'same as' } }  
if sameAsTaxon ~= '' then  
ok, info = pcall(.expandTemplate, , { title = 'Template:Taxonomy/' .. sameAsTaxon, args = {['machine code'] = item } })  
end  
end  
end  
end  
if ok then  
-- if item is 'link_text', trim info and check whether '(?)' needs to be added  
if item == 'link_text' then  
-- there is a newline at the end of linkText when taxonomy template has "|link = LINK_TARGET|LINK_TEXT"  
info = mw.text.trim(info)  
if string.sub(taxon, -2) == '/?' and not string.find(info, '?', 1, true) then  
info = info .. '<span>&nbsp;(?)</span>'  
end  
end  
else  
info = 'Template:Taxonomy/' .. taxon .. '' --error indicator in code before conversion to Lua  
end  
return ok, info  
end  

--============================ taxonLink ==================================  
Returns a wikilink to a taxon, if required including '†' before it and  
' (?)' after it, and optionally italicized or bolded without a wikilink.  
Usage:  

=============================================================================  
function p.taxonLink  
local taxon = .args['taxon'] or ''  
local extinct = string.lower(.args['extinct'] or '')  
local bold = .args['bold'] or ''  
local italic = .args['italic'] or ''  
local abbreviated = .args['abbreviated'] or ''  
local linkTarget = .args['link_target'] or ''  
local linkText = .args['link_text'] or .args['plain_link_text'] or '' --temporarily allow alternative args  
return l.makeLink(taxon, extinct, bold, italic, abbreviated, linkTarget, linkText)  
end  

--= = = = = = = = = = = = = = getTaxonLink = = = = = = = = = = = = = = = =  
Internal function to drive l.makeLink.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.getTaxonLink(, taxon, rank, bold, italic, abbreviated, virus)  
local ok, extinct = p.getTaxonInfoItem(, taxon, 'extinct')  
if italic == '' then  
italic = :expandTemplate{ title = 'Template:Is italic taxon', args = { rank, virus = virus } }  
end  
local ok, linkTarget = p.getTaxonInfoItem(, taxon, 'link_target')  
local ok, linkText = p.getTaxonInfoItem(, taxon, 'link_text')  
return l.makeLink(taxon, extinct, bold, italic, abbreviated, linkTarget, linkText)  
end  

--= = = = = = = = = = = = = = makeLink = = = = = = = = = = = = = = = = = =  
Actually make the link.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.makeLink(taxon, extinct, bold, italic, abbreviated, linkTarget, linkText)  
local dummy  
-- if link text is missing, try to find a replacement  
if linkText == '' then  
if string.find(taxon, 'Incertae sedis', 1, true) then  
linkText = "*incertae sedis*"  
linkTarget = 'Incertae sedis'  
else  
linkText, dummy = l.stripExtra(taxon)  
end  
end  
if linkTarget == '' then linkTarget = linkText end  
if italic == 'yes' then linkText = TaxonItalics.italicizeTaxonName(linkText, false, abbreviated=='yes') end  
local link = ''  
if bold == 'yes' then link = '<b>' .. linkText .. '</b>'  
else  
if linkTarget == linkText then link = linkText  
else link = linkTarget .. '|' .. linkText  
end  
link = * .. link .. *  
end  
if (extinct == 'yes' or extinct == 'true') and not string.find(link, '†', 1, true) then  
link = '<span>†</span>' .. link  
end  
if string.sub(taxon, -2) == '/?' and not string.find(link, '?', 1, true) then  
link = link .. '<span>&nbsp;(?)</span>'  
end  
return link  
end  

--========================== showRankTable ================================  
Returns a wikitable showing the ranks and their values as set up by  
getRankTable.  
Usage:  
=============================================================================  
function p.showRankTable  
local rankTable = l.getRankTable  
local res = '{|\n|+ Ranks checked in taxonomy templates\n! Rank !! Shown as !! Value\n'  
for k, v in pairs(rankTable) do  
local rankShown = :expandTemplate{ title = 'Template:Anglicise rank', args = { k } }  
res = res .. TableRow .. l.tableCell(k) .. l.tableCell(rankShown) .. l.tableCell(v)  
end  
return res .. TableEnd  
end  

--============================== find =====================================  
Returns the taxon above the specified taxon with a given rank.  
Usage:  
=============================================================================  
function p.find  
local currTaxon = .args[1] or ''  
if currTaxon == '' then return '<span>no taxon supplied</span>' end  
local rank = .args[2] or ''  
if rank == '' then return '<span>no rank supplied</span>' end  
local inHierarchy = true -- still in the taxonomic hierarchy or off the top?  
local searching = true -- still searching  
while inHierarchy and searching do  
local ok, parent = p.getTaxonInfoItem(, currTaxon, 'parent')  
if ok and parent ~= '' then  
currTaxon = parent  
local ok, currRank = p.getTaxonInfoItem(, currTaxon, 'rank')  
if currRank == rank then  
searching = false  
end  
else  
inHierarchy = false  
end  
end  
if inHierarchy and not searching then return currTaxon  
else return '<span>rank not found</span>'  
end  
end  

--=============================== nth =====================================  
External utility function primarily intended for use in checking and debugging.  
Returns the nth level above a taxon in a taxonomic hierarchy, where the taxon  
itself is counted as the first level.  
Usage:  
=============================================================================  
function p.nth  
local currTaxon = .args[1] or ''  
if currTaxon == '' then return 'ERROR: no taxon supplied' end  
local n = tonumber(.args['n'] or 1)  
if n > MaxSearchLevels then  
return 'Exceeded maximum number of levels allowed (' .. MaxSearchLevels .. ')'  
end  
local i = 1  
local inHierarchy = true -- still in the taxonomic hierarchy or off the top?  
while i < n and inHierarchy do  
local ok, parent = p.getTaxonInfoItem(, currTaxon, 'parent')  
if ok and parent ~= '' then  
currTaxon = parent  
i = i + 1  
else  
inHierarchy = false  
end  
end  
if inHierarchy then return currTaxon  
else return 'Level ' .. n .. ' is past the top of the taxonomic hierarchy'  
end  
end  

--============================= nLevels ===================================  
External utility function primarily intended for use in checking and debugging.  
Returns number of levels in a taxonomic hierarchy, starting from  
the supplied taxon as level 1.  
Usage:  
=============================================================================  
function p.nLevels  
local currTaxon = .args[1] or ''  
if currTaxon == '' then return 'ERROR: no taxon supplied' end  
local i = 1  
local inHierarchy = true -- still in the taxonomic hierarchy or off the top?  
while inHierarchy and i < MaxSearchLevels do  
local ok, parent = p.getTaxonInfoItem(, currTaxon, 'parent')  
if ok and parent ~= '' then  
currTaxon = parent  
i = i + 1  
else  
inHierarchy = false  
end  
end  
if inHierarchy then return MaxSearchLevels .. '+'  
else return i  
end  
end  

--============================= listAll ===================================  
External utility function primarily intended for use in checking and debugging.  
Returns a comma separated list of a taxonomic hierarchy, starting from  
the supplied taxon.  
Usage:  
=============================================================================  
function p.listAll  
local currTaxon = .args[1] or ''  
if currTaxon == '' then return 'ERROR: no taxon supplied' end  
return l.doListAll(l.makeTable(, currTaxon))  
end  

function l.doListAll(taxonTable, taxonRankTable)  
local lst = taxonTable[1] .. '-' .. tostring(taxonRankTable[1])  
for i = 2, taxonTable.n, 1 do  
lst = lst .. ', ' .. taxonTable[i] .. '-' .. taxonRankTable[i]  
end  
return lst  
end  

--=========================== removeQualifier ================================  
External utility function to remove a qualifier (any part after a "/") from a  
taxon name.  
Usage:  
=============================================================================  
function p.removeQualifier  
local baseName, qualifier = l.stripExtra(.args[1])  
return baseName  
end  

--=========================================================================  
Internal functions  
=============================================================================  

--= = = = = = = = = = = = stripExtra = = = = = = = = = = = = = = = = = = =  
Internal utility function to strip off any extra parts of a taxon name, i.e.  
anything after a '/'. Thus 'Felidae/?' would be split into 'Felidae' and '?'.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.stripExtra(taxonName)  
local i = mw.ustring.find(taxonName, '/', 1, true)  
if i then  
return mw.ustring.sub(taxonName, 1, i-1), mw.ustring.sub(taxonName, i, -1)  
else  
return taxonName, ''  
end  
end  

--= = = = = = = = = = = = splitTaxonName = = = = = = = = = = = = = = = = =  
Internal utility function to split a taxon name into its parts and return  
them. Possible formats include:  
- taxon
- taxon (disambig)
- taxon (Subgenus)
- taxon/qualifier
- combinations, e.g. taxon (disambig)/qualifier
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.splitTaxonName(taxon)  
-- get any qualifier present  
local qualifier = ''  
local i = mw.ustring.find(taxon, '/', 1, true)  
if i then  
qualifier = mw.ustring.sub(taxon, i+1, -1)  
taxon = mw.ustring.sub(taxon, 1, i-1)  
end  
-- get any disambiguator or subgenus  
local disambig = ''  
local subgenus = ''  
i = mw.ustring.find(taxon, ' (', 1, true)  
if i then  
local parenTerm = mw.ustring.sub(taxon, i+2, -2)  
taxon = mw.ustring.sub(taxon, 1, i-1)  
local char1 = mw.ustring.sub(parenTerm, 1, 1)  
if char1 == mw.ustring.lower(char1) then  
disambig = parenTerm  
else  
subgenus = parenTerm  
end  
end  
return taxon, disambig, subgenus, qualifier  
end  

--[[= = = = = = = = = = = = matchTaxonLink = = = = = = = = = = = = = = = = =  
Function to determine whether the taxon name derived from the name of the  
taxonomy template (passed in the parameter taxon) matches the link text  
(passed in the parameter linkText).  
The taxon name may have any of the formats:  
- baseTaxon/qualifier
- baseTaxon (disambig)
- baseTaxon (Subgenus) [distinguished by the capital letter]
- a qualifier may be present after the previous two formats.

Examples of matches (baseTaxon ~ linkText):  
- Pinus ~ Pinus
- Pinus sect. Trifoliae ~ Pinus sect. Trifoliae
- Pinus sect. Trifoliae ~ *Pinus* sect. *Trifoliae* [italic markers ignored]
- Pinus sect. Trifoliae ~ P. sect. Trifoliae [abbreviated genus name matches]
- Bombus (Pyrobombus) ~ Bombus (Pyrobombus)
- Bombus (Pyrobombus) ~ B. (Pyrobombus)
- Bombus (Pyrobombus) ~ Pyrobombus [link text may just be the subgenus]
- Heteractinida ~ "Heteractinida" [double-quotes are ignored in link text]
- "Heteractinida" ~ Heteractinida [double-quotes are ignored in base taxon name]
- Incertae sedis ~ anything [link text is ignored for matching in this case]
- Cetotheriidae with qualifier=? ~ Cetotheriidae (?)
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =]]  
function l.matchTaxonLink(taxon, linkText, rankBelowGenus)  
local dummy  
linkText, dummy = mw.ustring.gsub(linkText, "*", *) -- remove any italic wikitext in the link text  
linkText, dummy = mw.ustring.gsub(linkText, '<.->', '') -- strip all tags used to format the link text  
linkText, dummy = mw.ustring.gsub(linkText, '"', '') -- remove any occurrences of " in the link text  
local baseTaxon, disambig, subgenus, qualifier = l.splitTaxonName(taxon) -- split up the taxon name  
baseTaxon, dummy = mw.ustring.gsub(linkText, '"', '') -- remove any occurrences of " in the base taxon name  
local match = linkText == baseTaxon or  
linkText == subgenus or  
linkText == baseTaxon .. ' (' .. subgenus .. ')' or  
linkText == mw.ustring.sub(baseTaxon, 1, 1) .. '. (' .. subgenus .. ')' or  
baseTaxon == 'Incertae sedis' or  
rankBelowGenus and linkText == mw.ustring.gsub(baseTaxon, '([A-Z]).- (.*)', '%1. %2') or  
mw.ustring.find(qualifier, '?', 1, true) and mw.ustring.find(linkText, baseTaxon, 1, true) == 1  
return match  
end  

--[[= = = = = = = = = = = = = makeTable = = = = = = = = = = = = = = = = = = =  
Internal utility function to return a table (array) constructed from a  
taxonomic hierarchy stored in "Template:Taxonomy/..." templates.  
TABLE.n holds the total number of taxa; TABLE[1]..TABLE[TABLE.n] the taxon  
names.  
The last taxon in the table will either (a) have a taxonomy template but with  
no parent given (e.g. 'Life') or (b) not have a taxonomy template.  
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =]]  
function l.makeTable(, currTaxon)  
local taxonTable = {}  
local taxonRankTable = {}  
local ok, rank, parent  
local i = 1  
local topReached = false -- reached the top of the taxonomic hierarchy?  
repeat  
taxonTable[i] = currTaxon  
ok, rank = p.getTaxonInfoItem(, currTaxon, 'rank')  
if ok then taxonRankTable[i] = string.lower(rank) else taxonRankTable[i] = '' end  
ok, parent = p.getTaxonInfoItem(, currTaxon, 'parent')  
if ok and parent ~= '' then  
currTaxon = parent  
i = i + 1  
else  
topReached = true -- reached the top of the hierarchy or tried to use a non-existent taxonomy template  
end  
until topReached or i > MaxSearchLevels  
taxonTable.n = math.min(i, MaxSearchLevels)  
return taxonTable, taxonRankTable  
end  

--= = = = = = = = = = = = getRankTable = = = = = = = = = = = = = = = = = =  
Internal utility function to set up a table of numerical values corresponding  
to 'Linnaean' ranks, with upper ranks having higher values. In a valid  
taxonomic hierarchy, a lower rank should never have a higher value than a  
higher rank. The actual numerical values are arbitrary so long as they are  
ordered.  
The ranks should correspond to those in Template:Anglicise ranks.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.getRankTable  
return {  
classis = 1400,  
cohort = 1100,  
divisio = 1500,  
domain = 1700,  
epifamilia = 802,  
familia = 800,  
forma = 100,  
genus = 600,  
grandordo = 1005,  
['grandordo-mb'] = 1002,  
hyperfamilia = 805;  
infraclassis = 1397,  
infralegio = 1197,  
infraordo = 997,  
infraphylum = 1497,  
infraregnum = 1597,  
infratribus = 697,  
legio = 1200,  
magnordo = 1006,  
microphylum = 1495,  
micrordo = 995,  
mirordo = 1004,  
['mirordo-mb'] = 1001,  
nanophylum = 1494,  
nanordo = 994,  
ordo = 1000,  
parafamilia = 800,  
parvclassis = 1396; -- same as subterclassis  
parvordo = 996,  
phylum = 1500,  
regnum = 1600,  
sectio = 500,  
--series = 400, used too inconsistently to check  
species = 300,  
subclassis = 1398,  
subcohort = 1098,  
subdivisio = 1498,  
subfamilia = 798,  
subgenus = 598,  
sublegio = 1198,  
subordo = 998,  
subphylum = 1498,  
subregnum = 1598,  
subsectio = 498,  
subspecies = 298,  
subterclassis = 1396; -- same as parvclassis  
subtribus = 698,  
superclassis = 1403,  
supercohort = 1103,  
superdivisio = 1503,  
superdomain = 1703,  
superfamilia = 803,  
superlegio = 1203,  
superordo = 1003,  
superphylum = 1503,  
superregnum = 1603,  
supersectio = 503,  
supertribus = 703,  
tribus = 700,  
varietas = 200,  
zoodivisio = 1300,  
zoosectio = 900,  
zoosubdivisio = 1298,  
zoosubsectio = 898,  
}  
end  

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =  
Function to look up the arbitrary numerical value of a rank in a rank value  
table. "Ichno" and "oo" ranks are not stored separately, so if present the  
prefix is removed.  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.lookupRankVal(rankValTable, rank)  
local rankVal = rankValTable[rank]  
if not rankVal then  
-- may be an "ichno" or "oo" rank; try removing "ichno-" or "oo-"  
local baseRank = mw.ustring.gsub(mw.ustring.gsub(rank, '^ichno', *), '^oo', *)  
if baseRank == 'rdo' then baseRank = 'ordo' end  
-- if an "ichno" or "oo" rank, lower rank value slightly so it is ok below the base rank  
rankVal = rankValTable[baseRank]  
if rankVal then  
rankVal = rankVal - 0.1  
end  
end  
return rankVal  
end  

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =  
# = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.tableCell(arg1, arg2)  
local text, style  
if arg2 then  
style = arg1  
text = arg2  
else  
text = arg1  
end  
local res = '|'  
if style ~= '' then  
res = res .. style .. '|'  
end  
return res .. text .. '\n'  
end  

return p