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. ]]

local p = {}

--[[========================================================================= 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.

=
================================================================]] 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(frame) local currTaxon = frame.args[1] or '' local i = 1 -- count levels processed local searching = currTaxon ~= '' -- still searching for a colour? local foundICTaxon = false -- record whether 'incertae sedis' found local colour = '' -- default is no colour while searching and i <= MaxSearchLevels do		local plainCurrTaxon = p.stripExtra(currTaxon) -- remove trailing text after / if string.lower(plainCurrTaxon) == 'incertae sedis' then foundICTaxon = true else local possibleColour = frame: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(frame, 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 return colour elseif foundICTaxon then return frame:expandTemplate{ title = 'Template:Taxobox colour', args = { 'incertae sedis' } } elseif searching then -- hierarchy exceeds MaxSearchLevels levels return frame:expandTemplate{ title = 'Template:Taxobox/Error colour', args = { } } else return 'transparent' end end

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

=
================================================================]] function p.taxoboxList(frame) local currTaxon = frame.args[1] or '' local displayN = (tonumber(frame.args['display_taxa']) or 1) + 1 local auth = frame.args['authority'] or '' local parentAuth = frame.args['parent_authority'] or '' local gParentAuth = frame.args['gparent_authority'] or '' local ggParentAuth = frame.args['ggparent_authority'] or '' local gggParentAuth = frame.args['gggparent_authority'] or '' local boldFirst = frame.args['bold_first'] or 'link' -- values 'link' or 'bold' local taxonTable = p.makeTable(frame, currTaxon) local res = '' -- display all taxa above possible greatgreatgrandparent for i = taxonTable.n, 6, -1 do res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[i], fc = tostring(displayN >= i) } } end -- display greatgreatgrandparent, if it exists if taxonTable.n >= 5 then res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[5], authority = gggParentAuth, fc = tostring(displayN >= 5) } } end -- display greatgrandparent, if it exists; force the display if an infrataxon is below if taxonTable.n >= 4 then local force = tostring(displayN >= 4) or		             frame.expandTemplate{ title = 'Template:Infrataxon', args = { taxonTable[3] } } == 'true' or		              frame.expandTemplate{ title = 'Template:Infrataxon', args = { taxonTable[2] } } == 'true' res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[4], authority = ggParentAuth, fc = tostring(force) } } end -- display grandparent, if it exists; force the display if an infrataxon is below if taxonTable.n >= 3 then local force = tostring(displayN >= 3) or                     frame.expandTemplate{ title = 'Template:Infrataxon', args = { taxonTable[2] } } == 'true' res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[3], authority = gParentAuth, fc = tostring(force) } } end -- display parent, if it exists if taxonTable.n >= 2 then res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[2], authority = parentAuth, fc = tostring(displayN >= 2) } } end -- display target taxon res = res .. frame:expandTemplate{ title = 'Template:Taxobox/showtaxon', args = { taxonTable[1], authority = auth, fc = 'true', format = boldFirst } } return res end

--[[========================== taxonomyList ================================= Returns the cells of the taxonomy table displayed on "Template:Taxonomy...." pages. Usage:

=
================================================================]] function p.taxonomyList(frame) local currTaxon = frame.args[1] or '' if currTaxon == '' then return '| ||ERROR: no taxon supplied\n|-' end local taxonTable = p.makeTable(frame, currTaxon) local rankTable = p.getRankTable local lastRankVal = 1000000 local orderOk local res = '' for i = taxonTable.n, 1, -1 do		-- check ranks are in right order in the hierarchy local ok, rank = p.getTaxonInfoItem(frame, taxonTable[i], 'rank') local currRankVal = rankTable[rank] if currRankVal then orderOk = currRankVal < lastRankVal if orderOk then lastRankVal = currRankVal end else orderOk = true end -- now return a row of the taxonomy table with anomalous ranks marked if orderOk then res = res .. frame:expandTemplate{ title = 'Template:Taxonomy links', args = { taxonTable[i] } } else if i ~= 1 then res = res .. frame:expandTemplate{ title = 'Template:Taxonomy links', args = { taxonTable[i], error = 'true' } } else res = res .. frame:expandTemplate{ title = 'Template:Taxonomy links', args = { taxonTable[i], error = 'true', last_error = 'true' } } end end end 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 SAME_AS = 7 local PARENT = 1 local RANK = 2 local LINK_TARGET = 3 local LINK_TEXT = 4 local ALWAYS_DISPLAY = 5 local EXTINCT = 6 local REFS = 8

function p.callTaxonomyKey(frame) local parent = frame.args['parent'] or '' local rank = frame.args['rank'] or '' local extinct = string.lower(frame.args['extinct']) or '' local alwaysDisplay = string.lower(frame.args['always_display']) or '' local linkTarget = frame.args['link_target'] or '' local linkText = frame.args['link_text'] or  -- this is the "raw" link text, and can be  local refs = frame.args['refs'] or '' local sameAsTaxon = frame.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(frame.expandTemplate, frame, { 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 ' Error: attempt to follow two "same as" links :, but Template:Taxonomy/' .. sameAsTaxon .. ' also has a  parameter.' end else return frame:expandTemplate{ title = 'Template:Taxonomy key/missing template', args = {taxon=sameAsTaxon, msg='given as the value of '} } end end local link = linkTarget if linkText ~= '' and linkText ~= linkTarget then link = link .. "|" .. linkText end return frame:expandTemplate{ title = 'Template:Taxonomy key', args = {parent=parent, rank=rank, extinct=extinct, always_display=alwaysDisplay, link_target=linkTarget, link=link, refs=refs, same_as=sameAsTaxon} } 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', 'link', '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(frame) local taxon = frame.args[1] or '' local item = frame.args[2] or '' if item == '' then item = 'all' end local ok, info = p.getTaxonInfoItem(frame, taxon, item) return 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(frame) local taxon = frame.args['taxon'] or '' local extinct = string.lower(frame.args['extinct']) or '' local bold = frame.args['bold'] or '' local italic = frame.args['italic'] or '' local linkTarget = frame.args['link_target'] or '' local linkText = frame.args['link_text'] or frame.args['plain_link_text'] or '' --temporarily allow alternative args -- 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 = p.stripExtra(taxon) end end local link = '' if bold == 'yes' then link = ' .. linkText .. '	else if linkTarget == linkText or linkTarget == '' then link = linkText else link = linkTarget .. '|' .. linkText end link =  .. link ..  end if italic == 'yes' and not string.find(link, "", 1, true) then link = '' .. link ..  end if (extinct == 'yes' or extinct == 'true') and not string.find(link, '†', 1, true) then link = ' † ' .. link end if string.sub(taxon, -2) == '/?' and not string.find(link, '?', 1, true) then link = link .. ' (?) '	end return link end

--[[========================== showRankTable ================================ Returns a wikitable showing the ranks and their values as set up by getRankTable. Usage:

=
================================================================]]

function p.showRankTable(frame) local rankTable = p.getRankTable local res = '{| class="wikitable sortable"\n|+ Ranks checked in taxonomy templates\n! Rank !! Shown as !! Value\n' for k, v in pairs(rankTable) do		local rankShown = frame:expandTemplate{ title = 'Template:Anglicise rank', args = { k } } res = res .. '|-\n|' .. k .. '||' .. rankShown .. '||' .. v .. '\n' end return res .. '|}\n' 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(frame) local currTaxon = frame.args[1] or '' if currTaxon == '' then return 'ERROR: no taxon supplied' end local n = tonumber(frame.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(frame, 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(frame) local currTaxon = frame.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(frame, 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(frame) local currTaxon = frame.args[1] or '' if currTaxon == '' then return 'ERROR: no taxon supplied' end return p.listTaxa(p.makeTable(frame, currTaxon)) end

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

=
================================================================]]

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Internal utility function to strip off any extra parts of a taxon name, i.e. anything after a '/'. Thus "Felidae/?" would be reduced to "Felidae". = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = function p.stripExtra(taxonName) local i = string.find(taxonName,'/') if i then return string.sub(taxonName,1,i-1) else return taxonName end end

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Internal utility function to convert a taxon table to a comma-separated list. = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = function p.listTaxa(taxonTable) local lst = taxonTable[1] for i = 2, taxonTable.n, 1 do lst = lst .. ', ' .. taxonTable[i] end return lst end

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = Internal utility function to extract an item of information from a taxonomy template, following one 'same as' link if required. = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = function p.getTaxonInfoItem(frame, taxon, item) -- item == 'link' is a special case if item == 'link' then return p.getTaxonInfoLink(frame, taxon) end -- item ~= 'link' local ok, info = pcall(frame.expandTemplate, frame, { title = 'Template:Taxonomy/' .. taxon, args = {['machine code'] = item } }) if ok then if info == '' then -- try 'same as' local sameAsTaxon = frame:expandTemplate{ title = 'Template:Taxonomy/' .. taxon, args = {['machine code'] = 'same as' } } if sameAsTaxon ~= '' then ok, info = pcall(frame.expandTemplate, frame, { title = 'Template:Taxonomy/' .. sameAsTaxon, args = {['machine code'] = item } }) end end end if ok then -- if item is 'link_text' check whether '(?)' needs to be added if item == 'link_text' and string.sub(taxon, -2) == '/?' and not string.find(info, '?', 1, true) then info = info .. ' (?) '		end else info = 'Template:Taxonomy/' .. taxon .. '' --error indicator in code before conversion to Lua end return ok, info end

function p.getTaxonInfoLink(frame, taxon) local ok, linkText, linkTarget local link = '' ok, linkText = p.getTaxonInfoItem(frame, taxon, 'link_text') if ok then ok, linkTarget = p.getTaxonInfoItem(frame, taxon, 'link_target') if ok then if linkText == linkTarget then link = linkText else link = linkTarget .. '|' .. linkText end end end return ok, link end

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 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. = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = function p.makeTable(frame, currTaxon) local i = 1 local inHierarchy = true -- still in the taxonomic hierarchy or off the top? local taxonTable = {} taxonTable[1] = currTaxon; while i < MaxSearchLevels and inHierarchy do		local ok, parent = p.getTaxonInfoItem(frame, currTaxon, 'parent') if ok and parent ~= '' then currTaxon = parent i = i + 1 taxonTable[i] = currTaxon else inHierarchy = false -- run off the top of the hierarchy or tried to use non-existent taxonomy template end end taxonTable.n = i	return taxonTable end

--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = 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 p.getRankTable return { classis = 1400, cohort = 1100, divisio = 1500, domain = 1700, familia = 800, forma = 100, genus = 600, grandordo = 1005, ['grandordo-mb'] = 1002, 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 = 1499, nanordo = 999, ordo = 1000, parafamilia = 800, 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, subtribus = 698, superclassis = 1403, supercohort = 1103, superdivisio = 1503, superdomain = 1703, superfamilia = 803, superlegio = 1203, superordo = 1003, superphylum = 1503, superregnum = 1603, supertribus = 703, tribus = 700, varietas = 200, zoodivisio = 1300, zoosectio = 900, zoosubdivisio = 1298, zoosubsectio = 898, } end

return p