モジュール: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 = '
local TableEnd = '
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: TAXON

**=====================================================================**
function p.taxoboxColour(frame)
	return p.getTaxoboxColour(frame, frame.args1 or )
end
function p.getTaxoboxColour(frame, 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 = 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(frame, taxonTablei, taxonRankTablei, topTaxonN==i, authTablei, displayN >= i, , virus)
	end
	-- display target taxon, always displayed and emboldened
	res = res .. l.showTaxon(frame, taxonTable1, taxonRankTable1, topTaxonN==1, authTable1, true, boldFirst, virus)
	return res
end
--= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Show one taxon row in a taxobox.
**= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.showTaxon(frame, 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 frame: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 frame: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 frame: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(frame, taxon, 'always_display')
			force = alwaysDisplay == 'yes' or  alwaysDisplay == 'true'
		end
		if force then
			local res = l.tableCell(frame:expandTemplate{ title = 'Template:Anglicise rank', args = {rank} } .. ':')
			local bold = 'no'
			if boldFirst == 'bold' then bold = 'yes' end
			if auth ~=  then
				auth =  .. auth .. 
			end
			local res = res .. l.tableCell(l.getTaxonLink(frame, 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 right hand side of
"Template:Taxonomy...." pages.
Usage: TAXON

**=====================================================================**
function p.taxonomyList(frame)
	local currTaxon = frame.args1 or 
	if currTaxon ==  then
		return '
	end
	local taxonTable, taxonRankTable = l.makeTable(frame, 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  taxonTabletaxonTable.n == 'Ichnos' or taxonTabletaxonTable.n == 'Veterovata' then
			taxoColour = frame:expandTemplate{ title = 'Template:Taxobox colour', args = { taxonTabletaxonTable.n } }
		else
			taxoColour = frame:expandTemplate{ title = 'Template:Taxobox colour', args = { taxonTabletaxonTable.n - 1 } }
		end
	end
	if taxoColour == frame:expandTemplate{ title = 'Template:Taxobox colour', args = { 'virus' } } then
		virus = 'yes'
	end
	-- add information message
	local res = 'Bold ranks show taxa that will be shown in taxoboxesbecause rank is principal or always_display=yes.\n'
	-- start table
	res =  res .. '
	            .. taxoColour .. '"|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 = taxonTabletaxonTable.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', frame: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 right order in the hierarchy
		taxon = taxonTablei
		local rank = taxonRankTablei
		currRankVal = l.lookupRankVal(rankValTable, rank)
		if currRankVal then
			orderOk = currRankVal ' .. anglicizedRank .. ':'
	else
		cellContent = cellContent .. anglicizedRank .. ':'
	end
	if error == 'yes' then
		cellContent =  .. cellContent .. 
	end
	res = res .. l.tableCell(cellContent)
	          .. l.tableCell( .. link .. )
	          .. l.tableCell( .. qualifier  .. )
	          .. l.tableCell( .. frame:expandTemplate{ title = 'Template:Edit a taxon', args = { taxon } } .. )
	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(frame)
	local taxon = frame.args'taxon' or 
	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 sameAsInfoSAME_AS ==  then
				if parent ==  then parent = sameAsInfoPARENT end
				if rank ==  then rank = sameAsInfoRANK end
				if extinct ==  then extinct = string.lower(sameAsInfoEXTINCT) end
				if alwaysDisplay ==  then alwaysDisplay = string.lower(sameAsInfoALWAYS_DISPLAY) end
				if linkTarget ==  then linkTarget = sameAsInfoLINK_TARGET end
				if linkText ==  then linkText = sameAsInfoLINK_TEXT end
				if refs ==  and parent == sameAsInfoPARENT then refs = sameAsInfoREFS end
			else
				return 'Error: attempt to follow two "same as" links: same_as = ' .. sameAsTaxon .. ', but Template:Taxonomy/' .. sameAsTaxon .. ' also has asame_as parameter.'
			end
		else
			return frame:expandTemplate{ title = 'Template:Taxonomy key/missing template', args = {taxon=sameAsTaxon, msg='given as the value of same as'} }
		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(frame, parent, 'extinct')
		if ok and (parentExtinct == 'yes' or parentExtinct == 'true') then extinctError = 'yes' end
	end
	return frame: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: REFS

**=====================================================================**
function p.showRefs(frame)
	local taxonName = frame.args1 or 
	local refs = frame.args2 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 (incertae sedis)'
	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
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(frame)
	local taxon = frame.args1 or 
	local item = frame.args2 or 
	if item ==  then item = 'all' end
	local ok, info = p.getTaxonInfoItem(frame, 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(frame, taxon, item)
	local ok, info
	-- item == 'dagger' is a special case
	if item == 'dagger' then
		ok, info = p.getTaxonInfoItem(frame, taxon, 'extinct')
		if ok then
			if info == 'yes' or info == 'true' then
				info = '†'
			else
				info = 
			end
		end
	-- item ~= 'dagger'
	else
		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
	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 .. ' (?)'
			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(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 abbreviated = frame.args'abbreviated' 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
	return l.makeLink(taxon, extinct, bold, italic, abbreviated, linkTarget, linkText)
end
--= = = = = = = = = = = = = = getTaxonLink  = = = = = = = = = = = = = = = =
Internal function to drive l.makeLink().
**= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
function l.getTaxonLink(frame, taxon, rank, bold, italic, abbreviated, virus)
	local ok, extinct = p.getTaxonInfoItem(frame, taxon, 'extinct')
	if italic ==  then
		italic = frame:expandTemplate{ title = 'Template:Is italic taxon', args = { rank, virus = virus } }
	end
	local ok, linkTarget = p.getTaxonInfoItem(frame, taxon, 'link_target')
	local ok, linkText = p.getTaxonInfoItem(frame, 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 =  .. linkText .. 
	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 = '†' .. 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: showRankTable

**=====================================================================**
function p.showRankTable(frame)
	local rankTable = l.getRankTable()
	local res = '
Shown as 
Value\n'
	for k, v in pairs(rankTable) do
		local rankShown = frame: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: RANK

**=====================================================================**
function p.find(frame)
	local currTaxon = frame.args1 or 
	if currTaxon ==  then return 'no taxon supplied' end
	local rank = frame.args2 or 
	if rank ==  then return 'no rank supplied' 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(frame, currTaxon, 'parent')
			if ok and parent ~=  then
			currTaxon = parent
			local ok, currRank = p.getTaxonInfoItem(frame, currTaxon, 'rank')
			if currRank == rank then
				searching = false
			end
		else
			inHierarchy = false
		end
	end
	if inHierarchy and not searching then return currTaxon
	else return 'rank not found'
	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: n=N

**=====================================================================**
function p.nth(frame)
	local currTaxon = frame.args1 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 ', ) -- 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; TABLE1..TABLETABLE.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(frame, currTaxon)
	local taxonTable = {}
	local taxonRankTable = {}
	local ok, rank, parent
	local i = 1
	local topReached = false -- reached the top of the taxonomic hierarchy?
	repeat
		taxonTablei = currTaxon
		ok, rank = p.getTaxonInfoItem(frame, currTaxon, 'rank')
		if ok then taxonRankTablei = string.lower(rank) else taxonRankTablei =  end
		ok, parent = p.getTaxonInfoItem(frame, 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 = rankValTablerank
	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 = rankValTablebaseRank
		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
		style = 
		text = arg1
	end
	local res = '|'
	if style ~=  then
		res = res .. style .. '|'
	end
	return res .. text .. '\n'
end
return p