# Module:Taxobox

-- vim: set noexpandtab ft=lua ts=4 sw=4:  

local ENABLE_DEBUG = true  

local Cite = require('Module:Cite')  
local fb = require('Module:Fallback')  

local p = {} -- module exports  
local L = {} -- alias to local functions  
-- (so it can be iterated by p in debug mode)  
local _linkconfig -- use links from content language Wikipedia  
-- or from Wikidata, default to Wikidata  
local _contentlang  
local usereferences -- array of references to be prefered in the given order  
local usetaxa -- array of taxa to be preferred in the given order  
local hideranks -- array of ranks to be show or hide from display  
local code = false  
local visited = {}  
-- background colors for each code  
local colors = {  
[false] = '#d3d3d3',  
[13011] = '#d3d3a4', -- ICZN  
[693148] = '#9bcd9b', -- ICNafp  
[764] = '#a4d3d3', -- ICNCP  
[743780] = '#d3a4d3', -- BC/ICNP  
[14920640] = '#2f6fab', -- ICVCN  
}  

local i18nmessages = require("Module:I18n/taxobox")  

-- readable taxon properties  
local P_IMAGE = "P18"  
local P_INSTANCE_OF = "P31"  
local P_TAXON_RANK = "P105"  
local P_IUCN_STATUS = "P141"  
local P_TAXON_PARENT = "P171"  
local P_SPREAD_MAP = "P181"  
local P_TAXON_NAME = "P225"  
local P_STATED_IN = "P248"  
local P_AUTHOR = "P405"  
local P_AUTHOR_ABBR_IPNI = "P428"  
local P_ERA_START = "P523"  
local P_ERA_END = "P524"  
local P_BASINYM= "P566"  
local P_TAXON_YEAR = "P574"  
local P_END_TIME = "P582"  
local P_EX_AUTHOR = "P697"  
local P_AUTHOR_ABBR_ZOOLOGY = "P835"  
local P_NOMENCLATURE_CODE = "P944"  
local P_COMMON_NAME = "P1843"  

-- readable item  
local CLADE = 713623  
local GENUS = 34740  
local SUBGENUS = 3238261  
local RED_DATA_LIST = 32059  
local MONOTYPIC_TAXON = 310890  
local GEOLOGICAL_ERA = 630830  
local SYSTEMATICS = 3516404  
local RECOMBINATION = 14594740  
local EXTINCT = 237350  

local function capitalize(text)  
return mw.ustring.gsub(text, "^%l", mw.ustring.upper)  
end  

local function mergeTable(a, b)  
for _, value in ipairs(b) do  
a[#a + 1] = value  
end  
return a  
end  
L.mergeTable = mergeTable  

-- credit to http://lua-users.org/wiki/StringInterpolation  
local function namedStringFormat(str, vars)  
-- Allow replace_vars{str, vars} syntax as well as  
-- replace_vars(str, {vars})  
if not vars then  
vars = str  
str = vars[1]  
end  
return (string.gsub(str, "({([^}]+)})",  
function(whole,i)  
return vars[i] or whole  
end))  
end  
L.namedStringFormat = namedStringFormat  

local function setLang(contentlang)  
_contentlang = contentlang or "en"  
end  
L.setLang = setLang  

local function getLang  
return _contentlang  
end  
L.getLang = getLang  

local function i18n(str)  
local message = i18nmessages[str]  
if type(message) == 'string' then  
return message  
end  
return fb._langSwitch(message, getLang)  
end  
L.i18n = i18n  

-- parse item-ids like argument (like config[references]) which is a space  
-- separated list of item numbers like "Q1 Q2 Q3"  
local function parseItemIds(itemids)  
local items = {}  
local priority = 0  
if itemids then  
for word in string.gmatch(itemids, "%w+") do  
priority = priority + 1  
item = "Q" .. tonumber(string.sub(word, 2))  
items[item] = priority  
end  
end  
items.size = priority  
return items  
end  
L.parseItemIds = parseItemIds  

-- parse config arguments passed by #invode:taxobox. below are all we  
-- support currently:  
-- - config[lang]: set content language (default: en)  
-- - config[count]: maximum count of taxon to be recursively iterated  
-- - config[references]: references to be preffered in the given order  
-- - config[dryrun]: generate <pre> block instead of expanding template  
-- - config[link]: local or wikidata  
local function parseConfig(args)  
setLang(args["config[lang]"])  
local count = tonumber(args["config[count]"]) or 10  
if count > 25 then  
-- roughly about 100 expensive parser function calls  
error(i18n("taxon-count-too-high"))  
end  
usereferences = parseItemIds(args["config[references]"])  
usetaxa = parseItemIds(args["config[usetaxa]"])  

hideranks = {}  
local displaypattern = "^display%[([^]]+)%]$"  
local qidpattern = "^Q?(%d+)$"  
for k, v in pairs(args) do  
v = mw.ustring.lower(v)  
if string.match(k, displaypattern) then  
k = string.gsub(k, displaypattern, "%1")  
if string.match(k, qidpattern) then  
k = string.gsub(k, qidpattern, "%1")  
k = tonumber(k)  
end  
-- TODO: i18n?  
if ({n=true, no=true, ["false"]=true, hide=true})[v] then  
hideranks[k] = true  
end  
end  
end  

_linkconfig = "wikidata"  
if args["config[link]"] and  
mw.ustring.lower(args["config[link]"]) == "sitelink" then  
_linkconfig = "sitelink"  
end  

return {  
["count"] = count,  
["lang"] = lang,  
["dryrun"] = args["config[dryrun]"],  
["link"] = _linkconfig  
}  
end  

-- check if International Code of Nomenclature for algae, fungi, and plants (ICNafp) applies  
local function icnafpApplies  
return code == 693148  
end  
L.icnafpApplies = icnafpApplies  

-- the label of the item if present in the specified language or 'no label'  
-- TODO: merge with getLabel  
function getItemLabel(item, lang)  
local label = i18n('no-label')  
if item then  
label = item:getLabel(lang or getLang) or label  
end  
return label  
end  
L.getItemLabel = getItemLabel  

local function getLabel(id)  
if type(id) == "number" then  
id = "Q" .. id  
end  
return getItemLabel(mw.wikibase.getEntity(id))  
end  
L.getLabel = getLabel  

local function getLink(id, label, format, named)  
if type(id) == "number" then  
id = "Q" .. id  
end  
local link = id  
if _linkconfig == "sitelink" then  
link = mw.wikibase.sitelink(id)  
end  
label = label or getItemLabel(mw.wikibase.getEntity(id))  
format = format or "%s"  
if named then  
return namedStringFormat{format, link=link, label=label}  
else  
return string.format(format, link, label)  
end  
end  
L.getLink = getLink  

-- Collect all claims of the given property of the item  
-- Returns all claims and their references in tables combined by the claims' rank.  
-- result.preferred[target id of claim] = [target id of P248 reference]  
-- use only if the data type of the property is item  
local function targetIds(item, property)  
local claims = {preferred = {}, normal = {}, deprecated = {}}  
if item and item.claims and item.claims[property] then  
for _,claim in pairs(item.claims[property]) do  
if claim.mainsnak.datavalue and claim.mainsnak.datavalue.value then  
local valueid = claim.mainsnak.datavalue.value['numeric-id']  

local refids = {}  
if claim.references then  
for _,ref in pairs(claim.references) do  
for prop, refclaim in pairs(ref.snaks[P_STATED_IN] or {}) do  
refids[tostring('Q' .. refclaim.datavalue.value['numeric-id'])] = true  
end  
end  
end  
claims[claim.rank][valueid] = refids  
else  
claims[claim.rank]['novalue'] = true -- snaktype not value  
end  
end  
end  
return claims  
end  
L.targetIds = targetIds  

-- Gives the first highest ranked claim and its references.  
-- use only if the data type of the property is item  
local function targetId(item, property)  
local claims = targetIds(item, property)  
if next(claims.preferred) then  
return claims.preferred  
end  
if next(claims.normal) then  
return claims.normal  
end  
return claims.deprecated  
end  
L.targetId = targetId  

-- Collect all claims of the given property of the item  
-- Returns a triple of claims, their qualifiers, and their references in tables combined by the claims' rank.  
-- Use only if the data type of the property is string  
local function targetStrs(item, property)  
choosenclaim = {preferred = {}, normal = {}, deprecated = {}}  
choosenqualifiers = {preferred = {}, normal = {}, deprecated = {}}  
choosenreferences = {preferred = {}, normal = {}, deprecated = {}}  
if item and item.claims and item.claims[property] then  
for _,claim in pairs(item.claims[property]) do  
if claim.mainsnak and claim.mainsnak.datavalue then  
index = #choosenclaim[claim.rank] + 1  
mw.log(index, claim.mainsnak.datavalue.value)  

local refids = {}  
if claim.references then  
for _,ref in pairs(claim.references) do  
for prop, refclaim in pairs(ref.snaks[P_STATED_IN] or {}) do  
refids['Q' .. refclaim.datavalue.value['numeric-id']] = true  
mw.log('string ref', refclaim.datavalue.value['numeric-id'])  
end  
end  
end  
if claim.mainsnak.datatype == 'monolingualtext' then  
choosenclaim[claim.rank][index] = tostring(claim.mainsnak.datavalue.value)  
else  
choosenclaim[claim.rank][index] = tostring(claim.mainsnak.datavalue.value)  
end  
choosenqualifiers[claim.rank][index] = claim.qualifiers  
choosenreferences[claim.rank][index] = refids  
end  
end  
end  

return choosenclaim, choosenqualifiers, choosenreferences  
end  
L.targetStrs = targetStrs  

-- Gives the first highest ranked claim and its qualifiers and references.  
-- Use only if the data type of the property is string  
local function targetStr(item, property)  
choosenclaim, choosenqualifiers, choosenreferences = targetStrs(item, property)  

for _, priority in pairs({"preferred", "normal", "deprecated"}) do  
index = next(choosenclaim[priority])  
if index then  
return choosenclaim[priority][index],  
choosenqualifiers[priority][index],  
choosenreferences[priority][index]  
end  
end  
return  
end  
L.targetStr = targetStr  

-- helper function to merge all claims, regardless of rank  
local function mergeClaims(claims, qualifiers, references)  
local c = {}  
local q = {}  
local r = {}  
for _, priority in pairs({"preferred", "normal", "deprecated"}) do  
mergeTable(c, claims[priority] or {})  
mergeTable(q, qualifiers[priority] or {})  
mergeTable(r, references[priority] or {})  
end  
return c, q, r  
end  
L.mergeClaims = mergeClaims  

-- Use only if the data type of the property is time  
local function targetTime(item, property)  
if item and item.claims and item.claims[property] then  
for _,claim in pairs(item.claims[property]) do  
if claim.mainsnak and claim.mainsnak.datavalue then  
return claim.mainsnak.datavalue.value.time  
end  
end  
end  
end  
L.targetTime = targetTime  

-- same as targetId but for qualifiers  
-- TODO merge  
local function qualifierTargetId(qualifiers, property)  
local claims = {}  
if qualifiers and qualifiers[property] then  
for _,claim in pairs(qualifiers[property]) do  
local valueid = claim.datavalue.value['numeric-id']  
table.insert(claims, valueid)  
end  
end  
return claims  
end  
L.qualifierTargetId = qualifierTargetId  

-- same as targetTime but for qualifiers  
-- TODO merge  
local function qualifierTargetTime(qualifiers, property)  
local claims = {}  
if qualifiers and qualifiers[property] then  
for _,claim in pairs(qualifiers[property]) do  
if claim.datavalue then  
mw.log('time qualifier', property, claim.datavalue.value.time)  
return claim.datavalue.value.time  
end  
end  
end  
end  
L.qualifierTargetTime = qualifierTargetTime  

-- takes a list of item ids (the values of the given table) and creates wikilinks based on their labels  
local function createLinks(list, authorAbbreviation)  
local authors = {}  
for _,authorid in pairs(list) do  
if authorid then  
local author = mw.wikibase.getEntity('Q' .. authorid)  
if author then  
local label  
if authorAbbreviation then  
if icnafpApplies then  
-- get author abbrieviation per IPNI set  
if targetStr(author, P_AUTHOR_ABBR_IPNI) then  
label = targetStr(author, P_AUTHOR_ABBR_IPNI)  
end  
elseif targetStr(author, P_AUTHOR_ABBR_ZOOLOGY) then  
-- get zoologist author citation set  
label = targetStr(author, P_AUTHOR_ABBR_ZOOLOGY)  
end  
if not label then  
-- use the "last" name if no abbreviation found  
-- also don't use the translated name  
label = getItemLabel(author, "en")  
if label ~= i18n('no-label') then  
_, _, label = mw.ustring.find(label, "(%w+)$")  
end  
end  
end  
table.insert(authors, getLink(authorid, label))  
end  
end  
end  
return authors  
end  
L.createLinks = createLinks  

local function vernacularName(item)  
local vernacularname  
-- select vernacular name for current language  
if item.claims and item.claims[P_COMMON_NAME] then  
for _, claim in pairs(item.claims[P_COMMON_NAME]) do  
if claim.mainsnak and claim.mainsnak.datavalue and  
claim.mainsnak.datavalue.type == "monolingualtext" and  
claim.mainsnak.datavalue.value.language == getLang then  
vernacularname = claim.mainsnak.datavalue.value.text  
break  
end  
end  
if vernacularname == '' then  
vernacularname = nil  
end  
end  

if not vernacularname then  
-- test if item label is not one of the scientific names  
vernacularname = getItemLabel(item)  
scnames = mergeClaims(targetStrs(item, P_TAXON_NAME))  
for _, n in pairs(scnames) do  
if vernacularname == n then  
return  
end  
end  
end  
if vernacularname == i18n("no-label") then  
return  
end  
return capitalize(vernacularname)  
end  
L.vernacularName = vernacularName  

local function authorString(item, namequalifiers, pid)  
pid = pid or P_AUTHOR -- set default property  
local concatstr = ', '  
local authorids = qualifierTargetId(namequalifiers, pid) -- get qualifiers  
if not next(authorids) then -- no qualifiers found, check properties  
local authorset = targetId(item, pid)  
local authors = {}  
if authorset then -- create list from set  
authorids = {}  
for author,_ in pairs(authorset) do  
table.insert(authorids, author)  
end  
end  
end  
local authors = createLinks(authorids, true)  
if next(authors) then  
local last = table.remove(authors)  
local rest = ''  
if #authors > 0 then  
rest = table.concat(authors, concatstr) .. ' & '  
end  
return rest .. last  
end  
end  
L.authorString = authorString  

-- create the taxon authors string, including year, ex authors and authors of the basionym  
local function createAllAuthorsStr(item, namequalifiers, year)  
local authors = authorString(item, namequalifiers)  
local authorsstr = ''  
if authors or not year == '????' then  
if icnafpApplies then  
-- check for basionym  
local basionymids = targetId(item, P_BASINYM)  
local basionymstr = ''  
if next(basionymids) then  
local basionym = mw.wikibase.getEntity('Q' .. next(basionymids))  
local _,basionymnamequalifiers = targetStr(basionym, P_TAXON_NAME)  
basionymstr = createAllAuthorsStr(basionym, basionymnamequalifiers)  
if basionymstr ~= '' then  
basionymstr = '(' .. basionymstr .. ') '  
else  
-- indicate missing basionym author  
basionymstr = '(????) '  
end  
end  

-- check ex-authors  
local exauthors = authorString(nil, namequalifiers, P_EX_AUTHOR)  
exauthorsstr = ''  
mw.log(exauthors)  
if exauthors then  
exauthorsstr = exauthors .. ' ex '  
end  
authorsstr = basionymstr .. exauthorsstr .. authors  
if year then  
authorsstr = authorsstr .. ' (' .. year .. ')'  
end  
else  
authorsstr = authors .. ', ' .. year  

-- parentheses needed if instance of recombination  
local recombination = false  
for _,tid in pairs(qualifierTargetId(namequalifiers, P_INSTANCE_OF)) do  
if tid == RECOMBINATION then  
recombination = true  
end  
end  

if recombination then  
authorsstr = '(' .. authorsstr .. ')'  
end  
end  
end  
return authorsstr  
end  
L.createAllAuthorsStr = createAllAuthorsStr  

-- show the stratigraphic range in which an extinct fossil existed  
local function fossilParams(item, params)  
local era1, era1references = next(targetId(item, P_ERA_START))  
local era2, era2references = next(targetId(item, P_ERA_END))  
if era1 and era2 and not (era1 == 'novalue' or era2 == 'novalue') then  
local era1item = mw.wikibase.getEntity("Q" .. era1)  
if era1item then  
params["era[1][id]"] = era1  
params["era[1][label]"] = getItemLabel(era1item)  
if not (era1 == era2) then  
local era2item = mw.wikibase.getEntity("Q" .. era2)  
if era2item then  
params["era[2][id]"] = era2  
params["era[2][label]"] = getItemLabel(era2item)  
end  
end  

-- merge references from era2 to era1, only show once  
for a, b in pairs(era2references) do  
era1references[a] = b  
end  

-- TODO: return data structure instead of pure str here  
params["era[references]"] = era1references  
end  
end  
end  
L.fossilParams = fossilParams  

-- returns html for the given refids set  
-- parameters:  
-- refids: list of integer ID to create a list of <ref>-references  
local function references(refids)  
local = mw.getCurrentFrame  
local refstr = ''  
if refids then  
for id,_ in pairs(refids) do  
local ref = Cite.citeitem(id, getLang) or 'Error during creation of citation. Please report ' .. id .. ' at Module_talk:Cite'  
mw.log('refstr for ', id, ref)  
refstr = refstr .. :extensionTag('ref', ref, {name=id})  
end  
end  
return refstr  
end  
L.references = references  

local function i18nByLatin(ranklatin, str, default)  
local suc, format = pcall(i18n, str .. "-" .. ranklatin)  
if not suc then  
format = default  
end  
return format  
end  
L.i18nByLatin = i18nByLatin  

local function formatScientificName(ranklatin, scientific, short)  
local pf = "scientific-name"  
if short then  
pf = "short-" .. pf  
end  
scipattern = i18nByLatin(  
ranklatin, pf .. "-pattern", i18n(pf .. "-pattern"))  
scirepl = i18nByLatin(  
ranklatin, pf .. "-repl", i18n(pf .. "-repl"))  
scientific = string.gsub(scientific, scipattern, scirepl)  
for scipattern, scirepl in pairs(  
i18nByLatin(ranklatin, pf .. "-replaces", i18n(pf .. "-replaces"))) do  
scientific = string.gsub(scientific, scipattern, scirepl)  
end  
return scientific  
end  
L.formatScientificName = formatScientificName  

local function renderTableHead(text, color)  
local  
if color then  
bgcolor = bgcolor .. " background-color: " .. color .. ";"  
end  
return mw.text.tag('tr', {}, mw.text.tag('th', {, .. bgcolor  
}, text))  
end  
L.renderTableHead = renderTableHead  

local function renderTableRow(text, extra_css)  
local css = "text-align: ;"  
if extra_css then  
css = css .. " " .. extra_css  
end  
return mw.text.tag('tr', {}, mw.text.tag('td',  
{, style=css}, text))  
end  
L.renderTableRow = renderTableRow  

local function renderFossilEra(params)  
local eralink = {}  
local refstr = references(params["era[references]"])  
for i = 1, 2 do  
local eraid = params[string.format("era[%d][id]", i)]  
if eraid then  
eralink[#eralink + 1] = getLink(eraid)  
end  
end  
return renderTableHead(getLink(GEOLOGICAL_ERA) .. refstr, params.color) ..  
renderTableRow(table.concat(eralink, '&mdash;'))  
end  
L.renderFossilEra = renderFossilEra  

local function renderIUCNStatus(params)  
local r = {}  
refstr = references(params["iucn_status[references]"])  
r[#r + 1] = renderTableHead(  
getLink(RED_DATA_LIST, getLabel(P_IUCN_STATUS)) .. refstr, params.color)  
-- TODO: support SVG systemLanguage here  
r[#r + 1] = renderTableRow(  
"[[File:" .. params["iucn_status[image]"] ..  
"|" .. params["iucn_status[label]"] .. "]]")  
return table.concat(r)  
end  
L.renderIUCNStatus = renderIUCNStatus  

local function formatTaxon(  
latin, qid, scientific, vernacular, is_subject, is_extinct)  
local nameformat  
scientific = scientific or i18n("no-scientific-name")  
local scientificshort = scientific  
if latin then  
scientificshort = formatScientificName(latin, scientific, true)  
scientific = formatScientificName(latin, scientific)  
end  
local nf = "item-format-parent"  
if is_subject then  
nf = "item-format-current"  
end  
if vernacular then  
nameformat = i18n(nf .. "-with-vernacular-name")  
else  
nameformat = i18n(nf .. "-without-vernacular-name")  
end  
local link = qid  
if _linkconfig == "sitelink" then  
link = mw.wikibase.sitelink(qid)  
end  
if is_extinct then  
nameformat = i18n("extinct-mark") .. nameformat  
end  
return namedStringFormat{  
nameformat, link=link, vernacular=vernacular,  
scientific=scientific, scientificshort=scientificshort},  
scientific  
end  
L.formatTaxon = formatTaxon  

local function renderRank(i, params)  
local row  
local nameformat  
local detailrows = {}  
local pf = string.format("rank[%d]", i)  
local ranklink = i18n("unknown-rank")  
local rankid = params[pf .. "[id]"]  
local ranklatin = params[pf .. "[latin]"]  
local is_subject = params[pf .. "[is_subject]"]  
local scientific = params[pf .. "[scientific]"]  
local formatted = params[pf .. "[taxon]"]  
if rankid then  
local linkformat = i18nByLatin(  
ranklatin, "rank-format", i18n("rank-format"))  
ranklink = getLink(rankid, nil, linkformat, true)  
end  
row = mw.text.tag(  
'tr', {},  
mw.text.tag('td', {}, ranklink) ..  
mw.text.tag('td', {}, formatted))  
if is_subject then  
local refstr = references(params[pf .. "[references]"])  
local authority = params[pf .. "[authority]"]  
detailrows = {  
renderTableHead(  
string.format(i18n("scientific-name-of-taxon"), getLabel(rankid))  
.. refstr, params.color  
),  
renderTableRow(scientific),  
renderTableRow(authority, "font-variant: small-caps;")  
}  
end  
return row, detailrows  
end  
L.renderRank = renderRank  

-- in case of more than one parent taxa: choose target according to the  
-- references selected by usereferences  
local function chooseParent(item)  
local cand  
local nextparent = {}  
for id,refs in pairs(targetId(item, P_TAXON_PARENT)) do  

-- some taxon, like Q2382443, the parent taxon is null  
local novalue = id == "novalue"  

-- try to find match from usetaxa  
if not novalue and usetaxa["Q" .. id] then  
table.insert(nextparent, {usetaxa["Q" .. id], id, refs})  
end  

-- or according to usereferences  
if refs and type(refs) ~= "boolean" then  
for r, i in pairs(usereferences) do  
if refs[r] then  
table.insert(nextparent, {i + usetaxa.size, id, refs})  
end  
end  
end  

if not novalue and not cand then -- if no item had references yet  
cand = {nil, id, refs} -- use this  
end  
end  
-- nextparent is not sorted, so sort it  
table.sort(nextparent, function(a, b)  
return a[1] < b[1]  
end)  

if next(nextparent) then  
_, cand = next(nextparent)  
end  

if cand and cand[1] == nil and cand[3] then  
for targetid, _ in pairs(cand[3]) do  
usereferences.size = usereferences.size + 1  
usereferences[targetid] = usereferences.size  
end  
end  
if cand then  
return cand[2], cand[3] or {}  
else  
return nil, {}  
end  
end  
L.chooseParent = chooseParent  

-- Find out if this taxon is extinct already  
local function isExtinct(item)  

-- check IUCN status  
local statusid, _ = next(targetId(item, P_IUCN_STATUS))  
if statusid == EXTINCT then  
return true  
end  

-- check temporal range end  
local eraend, _ = next(targetId(item, P_ERA_END))  
if eraend then  
return true  
end  

-- check end time  
local endtime = targetTime(item, P_END_TIME)  
if endtime then  
return true  
end  

return false  
end  
L.isExtinct = isExtinct  

-- Find out if the item is a monotypic taxon  
local function isMonotypic(item)  
return next(targetId(item, P_INSTANCE_OF)) == MONOTYPIC_TAXON  
end  
L.isMonotypic = isMonotypic  

local function taxonParams(qid, item, params, fetch_detail, is_parent_extinct)  
local rankid  
local ranklatin  
local is_subject  
local level = params["rank[size]"] + 1  
local pf = string.format("rank[%d]", level)  
local name, namequalifiers, namereferences = targetStr(item, P_TAXON_NAME)  
local vernacular = vernacularName(item)  
local is_extinct = is_parent_extinct or isExtinct(item)  

if rankid == SUBGENUS and  
string.match(name, "^%w+$") and  
params[string.format("rank[%d][id]", level - 1)] == GENUS then  
-- follow ICZN to prepend genus name in front of subgenus name  
name = string.format("%s (%s)",  
params[string.format("rank[%d][raw_scientific]", level - 1)],  
mw.ustring.lower(name))  
end  

rankid = next(targetId(item, P_TAXON_RANK))  
if rankid == "novalue" then  
rankid = CLADE  
end  

if rankid then  
local rankitem = mw.wikibase.getEntity('Q' .. rankid)  
ranklatin = rankitem:getLabel('la')  
end  
if ranklatin then  
ranklatin = mw.ustring.lower(ranklatin)  
end  

if (hideranks[rankid] or hideranks[ranklatin]) and not usetaxa[qid] then  
-- interrupt since this rank has been hided from display  
return params, is_extinct  
end  

name = capitalize(name)  
params["rank[size]"] = level  
params[pf .. "[id]"] = rankid  
params[pf .. "[link]"] = qid  
params[pf .. "[is_monotypic]"] = isMonotypic(item)  
params[pf .. "[vernacular]"] = vernacular  
params[pf .. "[raw_scientific]"] = name  
params[pf .. "[latin]"] = ranklatin  
params[pf .. "[is_extinct]"] = is_extinct  

if fetch_detail then  
-- get detail  
is_subject = true  
params[pf .. "[references]"] = namereferences  
year = qualifierTargetTime(namequalifiers, P_TAXON_YEAR) or  
targetTime(item, P_TAXON_YEAR)  
if year then  
-- access year in time representation "+1758-00-00T00:00:00Z"  
year = string.sub(year, 2, 5)  
else  
year = '????'  
end  
local authorsstr = createAllAuthorsStr(item, namequalifiers, year)  
params[pf .. "[authority]"] = authorsstr  
else  
is_subject = false  
end  

local formatted, sciname = formatTaxon(  
ranklatin, qid, name, vernacular, is_subject, is_extinct)  

params[pf .. "[scientific]"] = sciname  
params[pf .. "[is_subject]"] = is_subject  
params[pf .. "[taxon]"] = formatted  

return params, is_extinct  
end  
L.taxonParams = taxonParams  

-- performs the loop up the hierarchy using P_TAXON_PARENT  
local function iterateRanks(  
qid, count, fetch_detail, child_detailed, child_extinct, params)  
local inneritem  
local params = params or {["rank[size]"] = 0}  
local item = mw.wikibase.getEntity(qid)  

name = targetStr(item, P_TAXON_NAME)  
if name == 'nil' then  
return params, {}, item  
end  

local nextid, references = chooseParent(item)  
mw.log('nextid', nextid)  
if not code then  
codeid = next(targetId(item, P_NOMENCLATURE_CODE))  
if codeid and colors[codeid] then  
code = codeid  
end  
end  

if visited[nextid] then -- loop detection  
return params, {}, item  
elseif nextid then  
visited[nextid] = true  
end  

-- Monotypic taxon can contain extinct taxa,  
-- should not fetch detail in such circumstances  
-- for example: Q7105303  
local fetch_detail =  
fetch_detail or  
(child_detailed and not child_extinct and isMonotypic(item))  
local is_extinct, is_parent_extinct  

if nextid and (not code or count > 0) then  
params, refs, _, is_parent_extinct = iterateRanks(  
'Q' .. nextid, count - 1, false,  
fetch_detail, isExtinct(item), params)  
for ref, _ in pairs(refs) do  
references[ref] = true  
end  
end  
if count > 0 then  
params, is_extinct = taxonParams(  
qid, item, params, fetch_detail, is_parent_extinct)  
end  
params["code"] = code  
params["color"] = colors[code]  
return params, references, item, is_extinct  
end  
L.iterateRanks = iterateRanks  

-- use arguments from second table to override the first table  
-- support classical parameters like "species", "unranked_ordo"  
local function overrideParams(params, overrides)  
overrides = overrides or {}  

for key, val in pairs(overrides) do  
params[key] = overrides[key] or params[key]  
end  

-- classical taxonomic rank params  
local unranked = {}  
for i = 1, params["rank[size]"] do  
local pf = string.format("rank[%d]", i)  
local latin = params[pf .. "[latin]"]  
if latin == "clade" then  
unranked[#unranked + 1] = i  
else  
local txarg = pf .. "[taxon]"  
local atarg = pf .. "[authority]"  
params[txarg] = overrides[latin] or params[txarg]  
params[atarg] = overrides[latin .. "_authority"] or params[atarg]  
for j = #unranked, 1, -1 do  
local txarg = string.format("rank[%d][taxon]", unranked[j])  
local atarg = string.format("rank[%d][authority]", unranked[j])  
local argname = string.format("unranked_%s", latin)  
if j == #unranked then  
params[txarg] = overrides[argname] or  
override[argname .. "1"] or params[txarg]  
params[atarg] = overrides[argname .. "_authority"] or  
override[argname .. "1_authority"] or params[atarg]  
else  
params[txarg] = overrides[string.format('%s%d',  
argname, #unranked - j + 1)] or params[txarg]  
params[atarg] = overrides[string.format('%s%d_authority',  
argname, #unranked - j + 1)] or params[atarg]  
end  
end  
unranked = {}  
end  
if latin == "species" then  
local scarg = pf .. "[scientific]"  
params[scarg] = overrides["binomial"] or params[scarg]  
elseif ({subspecies=true, varietas=true, forma=true})[latin] then  
local scarg = pf .. "[scientific]"  
params[scarg] = overrides["trinomial"] or params[scarg]  
end  
end  

return params  
end  
L.overrideParams = overrideParams  

-- fetch params should passed to taxobox for the given qid (e.g., qid=Q729412  
-- for Heloderma) and count higher levels of the taxon hierarchy.  
-- developers: use this method for tests in the debug console, e.g.,  
-- p.localFunction("getTaxoboxParams")('Q729412', 5)  
local function getTaxoboxParams(qid, count)  
visited = {}  
local params, references, item = iterateRanks(qid, count, true)  
if params["rank[size]"] == 0 then  
return {}  
end  

local scarg = string.format("rank[%d][scientific]", params["rank[size]"])  
local vnarg = string.format("rank[%d][vernacular]", params["rank[size]"])  
params["name"] = params[vnarg] or params[scarg]  

capitalize(getItemLabel(item))  

local image = targetStr(item, P_IMAGE)  
params["image"] = image  

fossilParams(item, params)  

params["rank[references]"] = references  

local map = targetStr(item, P_SPREAD_MAP)  
params["range_map"] = map  

local statusid, statusreferences = next(targetId(item, P_IUCN_STATUS))  
params["iucn_status[id]"] = statusid  
if statusid then  
local status = mw.wikibase.getEntity("Q" .. statusid)  
local img, imgqualifiers, imgreferences = targetStr(status, P_IMAGE)  
if img then  
params["iucn_status[label]"] = getItemLabel(status)  
params["iucn_status[references]"] = statusreferences  
params["iucn_status[image]"] = img  
end  
end  

local audio, audioreferences = targetStr(item, P_AUDIO)  
params["audio"] = audio  

return params  
end  
L.getTaxoboxParams = getTaxoboxParams  

local function callbackTaxobox(template, params, overrides, dryrun)  
local content = {}  
local = mw.getCurrentFrame  
params = overrideParams(params, overrides)  

for key, val in pairs(params) do  
if type(val) == "boolean" then  
val = val and "yes" or "no"  
elseif type(val) == "table" and  
string.match(key, "%[references%]$") then  
local refs = {}  
for r, _ in pairs(val) do  
table.insert(refs, r)  
end  
val = table.concat(refs, " ")  
end  
if dryrun then  
content[#content + 1] = string.format(  
"|%s = %s", key, val)  
else  
params[key] = val  
end  
end  

if dryrun then  
table.sort(content, function(a, b)  
a = string.gsub(a, "%[(%d+)%]", function(i)  
return "[" .. string.rep("0", 3 - #i) .. i .. "]"  
end)  
b = string.gsub(b, "%[(%d+)%]", function(i)  
return "[" .. string.rep("0", 3 - #i) .. i .. "]"  
end)  
return a < b  
end)  
content = "\n"  
content = :callParserFunction("#tag", "nowiki", content)  
return mw.text.tag("pre", {}, content)  
else  
return :expandTemplate{title=template, args=params}  
end  
end  
L.callbackTaxobox = callbackTaxobox  

-- creates the taxobox from giving params  
local function renderTaxobox(params, overrides)  
local content = {}  
params = overrideParams(params, overrides)  

local color = params.color  

-- title  
content[#content + 1] = renderTableHead(params.name, color)  

-- image  
if params.image then  
content[#content + 1] = renderTableRow(  
"")  
end  

-- fossil era  
if params["era[1][id]"] then  
content[#content + 1] = renderFossilEra(params)  
end  

-- systematics  
local refstr = references(params["rank[references]"])  
content[#content + 1] = renderTableHead(  
getLink(SYSTEMATICS) .. refstr, color)  

-- ranks  
if params["rank[size]"] > 0 then  
local taxondetails = {}  
for i = 1, params["rank[size]"] do  
local row, detailrows = renderRank(i, params)  
content[#content + 1] = row  
taxondetails[#taxondetails + 1] = table.concat(detailrows)  
end  
content[#content + 1] = table.concat(taxondetails)  
end  

-- range map  
if params.range_map then  
content[#content + 1] = renderTableHead(i18n('range-map'), color)  
content[#content + 1] = renderTableRow(  
"")  
end  

-- iucn status  
if params["iucn_status[id]"] then  
content[#content + 1] = renderIUCNStatus(params)  
end  

-- audio  
if params.audio then  
content[#content + 1] = renderTableHead(getLink(P_AUDIO), color)  
content[#content + 1] =  
renderTableRow("")  
end  

return mw.text.tag('table', {  
style =  
width: ; -width: ; float: ;  
-style: solid; background-color: #f9f9f9;  

}, table.concat(content))  
end  
L.renderTaxobox = renderTaxobox  

if debug then  
function p.debugParams(params)  
mw.log("Start of logging params")  
mw.log(string.rep("=", 20))  
for k, v in pairs(params) do  
mw.log(k, v)  
end  
mw.log("End of logging params")  
end  

function p.localFunction(name)  
return L[name]  
end  
end  

function p.taxobox  
local config = parseConfig(.args)  
local qid = .args.qid  
local params = getTaxoboxParams(qid, config.count)  
return renderTaxobox(params, .args)  
end  

function p.callback  
local config = parseConfig(.args)  
local qid = .args.qid  
local template = .args.template or "Taxobox"  
local params = getTaxoboxParams(qid, config.count)  
return callbackTaxobox(template, params, .args, config.dryrun)  
end  

return p