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(&quot;Module:I18n/taxobox&quot;)
 
 -- readable taxon properties
 local P_IMAGE = &quot;P18&quot;
 local P_INSTANCE_OF = &quot;P31&quot;
 local P_TAXON_RANK = &quot;P105&quot;
 local P_IUCN_STATUS = &quot;P141&quot;
 local P_TAXON_PARENT = &quot;P171&quot;
 local P_SPREAD_MAP = &quot;P181&quot;
 local P_TAXON_NAME = &quot;P225&quot;
 local P_STATED_IN = &quot;P248&quot;
 local P_AUTHOR = &quot;P405&quot;
 local P_AUTHOR_ABBR_IPNI = &quot;P428&quot;
 local P_ERA_START = &quot;P523&quot;
 local P_ERA_END = &quot;P524&quot;
 local P_BASINYM= &quot;P566&quot;
 local P_TAXON_YEAR = &quot;P574&quot;
 local P_END_TIME = &quot;P582&quot;
 local P_EX_AUTHOR = &quot;P697&quot;
 local P_AUTHOR_ABBR_ZOOLOGY = &quot;P835&quot;
 local P_NOMENCLATURE_CODE = &quot;P944&quot;
 local P_COMMON_NAME = &quot;P1843&quot;
 
 -- 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, &quot;^%l&quot;, 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, &quot;({([^}]+)})&quot;,
 		function(whole,i)
 			return vars[i] or whole
 		end))
 end
 L.namedStringFormat = namedStringFormat
 
 
 local function setLang(contentlang)
 	_contentlang = contentlang or &quot;en&quot;
 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 &quot;Q1 Q2 Q3&quot;
 local function parseItemIds(itemids)
 	local items = {}
 	local priority = 0
 	if itemids then
 		for word in string.gmatch(itemids, &quot;%w+&quot;) do
 			priority = priority + 1
 			item = &quot;Q&quot; .. 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 &lt;pre&gt; block instead of expanding template
 -- - config[link]: local or wikidata
 local function parseConfig(args)
 	setLang(args[&quot;config[lang]&quot;])
 	local count = tonumber(args[&quot;config[count]&quot;]) or 10
 	if count &gt; 25 then
 		-- roughly about 100 expensive parser function calls
 		error(i18n(&quot;taxon-count-too-high&quot;))
 	end
 	usereferences = parseItemIds(args[&quot;config[references]&quot;])
 	usetaxa = parseItemIds(args[&quot;config[usetaxa]&quot;])
 
 	hideranks = {}
 	local displaypattern = &quot;^display%[([^]]+)%]$&quot;
 	local qidpattern = &quot;^Q?(%d+)$&quot;
 	for k, v in pairs(args) do
 		v = mw.ustring.lower(v)
 		if string.match(k, displaypattern) then
 			k = string.gsub(k, displaypattern, &quot;%1&quot;)
 			if string.match(k, qidpattern) then
 				k = string.gsub(k, qidpattern, &quot;%1&quot;)
 				k = tonumber(k)
 			end
 			-- TODO: i18n?
 			if ({n=true, no=true, [&quot;false&quot;]=true, hide=true})[v] then
 				hideranks[k] = true
 			end
 		end
 	end
 
 	_linkconfig = &quot;wikidata&quot;
 	if args[&quot;config[link]&quot;] and
 		mw.ustring.lower(args[&quot;config[link]&quot;]) == &quot;sitelink&quot; then
 		_linkconfig = &quot;sitelink&quot;
 	end
 
 	return {
 		[&quot;count&quot;] = count,
 		[&quot;lang&quot;] = lang,
 		[&quot;dryrun&quot;] = args[&quot;config[dryrun]&quot;],
 		[&quot;link&quot;] = _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) == &quot;number&quot; then
 		id = &quot;Q&quot; .. id
 	end
 	return getItemLabel(mw.wikibase.getEntity(id))
 end
 L.getLabel = getLabel
 
 
 local function getLink(id, label, format, named)
 	if type(id) == &quot;number&quot; then
 		id = &quot;Q&quot; .. id
 	end
 	local link = id
 	if _linkconfig == &quot;sitelink&quot; then
 		link = mw.wikibase.sitelink(id)
 	end
 	label = label or getItemLabel(mw.wikibase.getEntity(id))
 	format = format or &quot;[[%s|%s]]&quot;
 	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({&quot;preferred&quot;, &quot;normal&quot;, &quot;deprecated&quot;}) 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({&quot;preferred&quot;, &quot;normal&quot;, &quot;deprecated&quot;}) 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 &quot;last&quot; name if no abbreviation found
 						-- also don't use the translated name
 						label = getItemLabel(author, &quot;en&quot;)
 						if label ~= i18n('no-label') then
 							_, _, label = mw.ustring.find(label, &quot;(%w+)$&quot;)
 						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 == &quot;monolingualtext&quot; 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(&quot;no-label&quot;) 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 &gt; 0 then
 			rest = table.concat(authors, concatstr) .. ' &amp; '
 		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(&quot;Q&quot; .. era1)
 		if era1item then
 			params[&quot;era[1][id]&quot;] = era1
 			params[&quot;era[1][label]&quot;] = getItemLabel(era1item)
 			if not (era1 == era2) then
 				local era2item = mw.wikibase.getEntity(&quot;Q&quot; .. era2)
 				if era2item then
 					params[&quot;era[2][id]&quot;] = era2
 					params[&quot;era[2][label]&quot;] = 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[&quot;era[references]&quot;] = 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 &lt;ref&gt;-references
 local function references(refids)
 	local frame = 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 .. frame: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 .. &quot;-&quot; .. ranklatin)
 	if not suc then
 		format = default
 	end
 	return format
 end
 L.i18nByLatin = i18nByLatin
 
 
 local function formatScientificName(ranklatin, scientific, short)
 	local pf = &quot;scientific-name&quot;
 	if short then
 		pf = &quot;short-&quot; .. pf
 	end
 	scipattern = i18nByLatin(
 		ranklatin, pf .. &quot;-pattern&quot;, i18n(pf .. &quot;-pattern&quot;))
 	scirepl = i18nByLatin(
 		ranklatin, pf .. &quot;-repl&quot;, i18n(pf .. &quot;-repl&quot;))
 	scientific = string.gsub(scientific, scipattern, scirepl)
 	for scipattern, scirepl in pairs(
 		i18nByLatin(ranklatin, pf .. &quot;-replaces&quot;, i18n(pf .. &quot;-replaces&quot;))) do
 		scientific = string.gsub(scientific, scipattern, scirepl)
 	end
 	return scientific
 end
 L.formatScientificName = formatScientificName
 
 
 local function renderTableHead(text, color)
 	local bgcolor = &quot;&quot;
 	if color then
 		bgcolor = bgcolor .. &quot; background-color: &quot; .. color .. &quot;;&quot;
 	end
 	return mw.text.tag('tr', {}, mw.text.tag('th', {
 		colspan='2',
 		style=&quot;text-align: center;&quot; .. bgcolor
 	}, text))
 end
 L.renderTableHead = renderTableHead
 
 
 local function renderTableRow(text, extra_css)
 	local css = &quot;text-align: center;&quot;
 	if extra_css then
 		css = css .. &quot; &quot; .. extra_css
 	end
 	return mw.text.tag('tr', {}, mw.text.tag('td',
 		{colspan='2', style=css}, text))
 end
 L.renderTableRow = renderTableRow
 
 
 local function renderFossilEra(params)
 	local eralink = {}
 	local refstr = references(params[&quot;era[references]&quot;])
 	for i = 1, 2 do
 		local eraid = params[string.format(&quot;era[%d][id]&quot;, i)]
 		if eraid then
 			eralink[#eralink + 1] = getLink(eraid)
 		end
 	end
 	return renderTableHead(getLink(GEOLOGICAL_ERA) .. refstr, params.color) ..
 		renderTableRow(table.concat(eralink, '&amp;mdash;'))
 end
 L.renderFossilEra = renderFossilEra
 
 
 local function renderIUCNStatus(params)
 	local r = {}
 	refstr = references(params[&quot;iucn_status[references]&quot;])
 	r[#r + 1] = renderTableHead(
 		getLink(RED_DATA_LIST, getLabel(P_IUCN_STATUS)) .. refstr, params.color)
 	-- TODO: support SVG systemLanguage here
 	r[#r + 1] = renderTableRow(
 		&quot;[[File:&quot; .. params[&quot;iucn_status[image]&quot;] ..
 		&quot;|220px|&quot; .. params[&quot;iucn_status[label]&quot;] .. &quot;]]&quot;)
 	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(&quot;no-scientific-name&quot;)
 	local scientificshort = scientific
 	if latin then
 		scientificshort = formatScientificName(latin, scientific, true)
 		scientific = formatScientificName(latin, scientific)
 	end
 	local nf = &quot;item-format-parent&quot;
 	if is_subject then
 		nf = &quot;item-format-current&quot;
 	end
 	if vernacular then
 		nameformat = i18n(nf .. &quot;-with-vernacular-name&quot;)
 	else
 		nameformat = i18n(nf .. &quot;-without-vernacular-name&quot;)
 	end
 	local link = qid
 	if _linkconfig == &quot;sitelink&quot; then
 		link = mw.wikibase.sitelink(qid)
 	end
 	if is_extinct then
 		nameformat = i18n(&quot;extinct-mark&quot;) .. 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(&quot;rank[%d]&quot;, i)
 	local ranklink = i18n(&quot;unknown-rank&quot;)
 	local rankid = params[pf .. &quot;[id]&quot;]
 	local ranklatin = params[pf .. &quot;[latin]&quot;]
 	local is_subject = params[pf .. &quot;[is_subject]&quot;]
 	local scientific = params[pf .. &quot;[scientific]&quot;]
 	local formatted = params[pf .. &quot;[taxon]&quot;]
 	if rankid then
 		local linkformat = i18nByLatin(
 			ranklatin, &quot;rank-format&quot;, i18n(&quot;rank-format&quot;))
 		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 .. &quot;[references]&quot;])
 		local authority = params[pf .. &quot;[authority]&quot;]
 		detailrows = {
 			renderTableHead(
 				string.format(i18n(&quot;scientific-name-of-taxon&quot;), getLabel(rankid))
 				.. refstr, params.color
 			),
 			renderTableRow(scientific),
 			renderTableRow(authority, &quot;font-variant: small-caps;&quot;)
 		}
 	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 == &quot;novalue&quot;
 
 		-- try to find match from usetaxa
 		if not novalue and usetaxa[&quot;Q&quot; .. id] then
 			table.insert(nextparent, {usetaxa[&quot;Q&quot; .. id], id, refs})
 		end
 
 		-- or according to usereferences
 		if refs and type(refs) ~= &quot;boolean&quot;  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] &lt; 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[&quot;rank[size]&quot;] + 1
 	local pf = string.format(&quot;rank[%d]&quot;, 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, &quot;^%w+$&quot;) and
 		params[string.format(&quot;rank[%d][id]&quot;, level - 1)] == GENUS then
 		-- follow ICZN to prepend genus name in front of subgenus name
 		name = string.format(&quot;%s (%s)&quot;,
 			params[string.format(&quot;rank[%d][raw_scientific]&quot;, level - 1)],
 			mw.ustring.lower(name))
 	end
 
 	rankid = next(targetId(item, P_TAXON_RANK))
 	if rankid == &quot;novalue&quot; 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[&quot;rank[size]&quot;] = level
 	params[pf .. &quot;[id]&quot;] = rankid
 	params[pf .. &quot;[link]&quot;] = qid
 	params[pf .. &quot;[is_monotypic]&quot;] = isMonotypic(item)
 	params[pf .. &quot;[vernacular]&quot;] = vernacular
 	params[pf .. &quot;[raw_scientific]&quot;] = name
 	params[pf .. &quot;[latin]&quot;] = ranklatin
 	params[pf .. &quot;[is_extinct]&quot;] = is_extinct
 
 	if fetch_detail then
 		-- get detail
 		is_subject = true
 		params[pf .. &quot;[references]&quot;] = namereferences
 		year = qualifierTargetTime(namequalifiers, P_TAXON_YEAR) or
 			targetTime(item, P_TAXON_YEAR)
 		if year then
 			-- access year in time representation &quot;+1758-00-00T00:00:00Z&quot;
 			year = string.sub(year, 2, 5)
 		else
 			year = '????'
 		end
 		local authorsstr = createAllAuthorsStr(item, namequalifiers, year)
 		params[pf .. &quot;[authority]&quot;] = authorsstr
 	else
 		is_subject = false
 	end
 
 	local formatted, sciname = formatTaxon(
 		ranklatin, qid, name, vernacular, is_subject, is_extinct)
 
 	params[pf .. &quot;[scientific]&quot;] = sciname
 	params[pf .. &quot;[is_subject]&quot;] = is_subject
 	params[pf .. &quot;[taxon]&quot;] = 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 {[&quot;rank[size]&quot;] = 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 &gt; 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 &gt; 0 then
 		params, is_extinct = taxonParams(
 			qid, item, params, fetch_detail, is_parent_extinct)
 	end
 	params[&quot;code&quot;] = code
 	params[&quot;color&quot;] = colors[code]
 	return params, references, item, is_extinct
 end
 L.iterateRanks = iterateRanks
 
 -- use arguments from second table to override the first table
 -- support classical {{taxobox}} parameters like &quot;species&quot;, &quot;unranked_ordo&quot;
 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[&quot;rank[size]&quot;] do
 		local pf = string.format(&quot;rank[%d]&quot;, i)
 		local latin = params[pf .. &quot;[latin]&quot;]
 		if latin == &quot;clade&quot; then
 			unranked[#unranked + 1] = i
 		else
 			local txarg = pf .. &quot;[taxon]&quot;
 			local atarg = pf .. &quot;[authority]&quot;
 			params[txarg] = overrides[latin] or params[txarg]
 			params[atarg] = overrides[latin .. &quot;_authority&quot;] or params[atarg]
 			for j = #unranked, 1, -1 do
 				local txarg = string.format(&quot;rank[%d][taxon]&quot;, unranked[j])
 				local atarg = string.format(&quot;rank[%d][authority]&quot;, unranked[j])
 				local argname = string.format(&quot;unranked_%s&quot;, latin)
 				if j == #unranked then
 					params[txarg] = overrides[argname] or
 						override[argname .. &quot;1&quot;] or params[txarg]
 					params[atarg] = overrides[argname .. &quot;_authority&quot;] or
 						override[argname .. &quot;1_authority&quot;] 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 == &quot;species&quot; then
 			local scarg = pf .. &quot;[scientific]&quot; 
 			params[scarg] = overrides[&quot;binomial&quot;] or params[scarg]
 		elseif ({subspecies=true, varietas=true, forma=true})[latin] then
 			local scarg = pf .. &quot;[scientific]&quot; 
 			params[scarg] = overrides[&quot;trinomial&quot;] 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(&quot;getTaxoboxParams&quot;)('Q729412', 5)
 local function getTaxoboxParams(qid, count)
 	visited = {}
 	local params, references, item = iterateRanks(qid, count, true)
 	if params[&quot;rank[size]&quot;] == 0 then
 		return {}
 	end
 
 	local scarg = string.format(&quot;rank[%d][scientific]&quot;, params[&quot;rank[size]&quot;])
 	local vnarg = string.format(&quot;rank[%d][vernacular]&quot;, params[&quot;rank[size]&quot;])
 	params[&quot;name&quot;] = params[vnarg] or params[scarg]
 
 	capitalize(getItemLabel(item))
 
 	local image = targetStr(item, P_IMAGE)
 	params[&quot;image&quot;] = image
 
 	fossilParams(item, params)
 
 	params[&quot;rank[references]&quot;] = references
 
 	local map = targetStr(item, P_SPREAD_MAP)
 	params[&quot;range_map&quot;] = map
 
 	local statusid, statusreferences = next(targetId(item, P_IUCN_STATUS))
 	params[&quot;iucn_status[id]&quot;] = statusid
 	if statusid then
 		local status = mw.wikibase.getEntity(&quot;Q&quot; .. statusid)
 		local img, imgqualifiers, imgreferences = targetStr(status, P_IMAGE)
 		if img then
 			params[&quot;iucn_status[label]&quot;] = getItemLabel(status)
 			params[&quot;iucn_status[references]&quot;] = statusreferences
 			params[&quot;iucn_status[image]&quot;] = img
 		end
 	end
 
 	local audio, audioreferences = targetStr(item, P_AUDIO)
 	params[&quot;audio&quot;] = audio
 
 	return params
 end
 L.getTaxoboxParams = getTaxoboxParams
 
 
 local function callbackTaxobox(template, params, overrides, dryrun)
 	local content = {}
 	local frame = mw.getCurrentFrame()
 	params = overrideParams(params, overrides)
 
 	for key, val in pairs(params) do
 		if type(val) == &quot;boolean&quot; then
 			val = val and &quot;yes&quot; or &quot;no&quot;
 		elseif type(val) == &quot;table&quot; and
 			string.match(key, &quot;%[references%]$&quot;) then
 			local refs = {}
 			for r, _ in pairs(val) do
 				table.insert(refs, r)
 			end
 			val = table.concat(refs, &quot; &quot;)
 		end
 		if dryrun then
 			content[#content + 1] = string.format(
 				&quot;|%s = %s&quot;, key, val)
 		else
 			params[key] = val
 		end
 	end
 
 	if dryrun then
 		table.sort(content, function(a, b)
 			a = string.gsub(a, &quot;%[(%d+)%]&quot;, function(i)
 				return &quot;[&quot; .. string.rep(&quot;0&quot;, 3 - #i) .. i .. &quot;]&quot;
 			end)
 			b = string.gsub(b, &quot;%[(%d+)%]&quot;, function(i)
 				return &quot;[&quot; .. string.rep(&quot;0&quot;, 3 - #i) .. i .. &quot;]&quot;
 			end)
 			return a &lt; b
 		end)
 		content = &quot;{{&quot; .. template .. &quot;\n&quot; ..
 			table.concat(content, &quot;\n&quot;) .. &quot;\n}}\n&quot;
 		content = frame:callParserFunction(&quot;#tag&quot;, &quot;nowiki&quot;, content)
 		return mw.text.tag(&quot;pre&quot;, {}, content)
 	else
 		return frame: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(
 			&quot;[[File:&quot; .. params.image .. &quot;|220px|.]]&quot;)
 	end
 
 	-- fossil era
 	if params[&quot;era[1][id]&quot;] then
 		content[#content + 1] = renderFossilEra(params)
 	end
 
 	-- systematics
 	local refstr = references(params[&quot;rank[references]&quot;])
 	content[#content + 1] = renderTableHead(
 		getLink(SYSTEMATICS) .. refstr, color)
 
 	-- ranks
 	if params[&quot;rank[size]&quot;] &gt; 0 then
 		local taxondetails = {}
 		for i = 1, params[&quot;rank[size]&quot;] 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(
 			&quot;[[File:&quot; .. params.range_map .. &quot;|220px|.]]&quot;)
 	end
 
 	-- iucn status
 	if params[&quot;iucn_status[id]&quot;] then
 		content[#content + 1] = renderIUCNStatus(params)
 	end
 
 	-- audio
 	if params.audio then
 		content[#content + 1] = renderTableHead(getLink(P_AUDIO), color)
 		content[#content + 1] =
 			renderTableRow(&quot;[[File:&quot; .. params.audio .. &quot;]]&quot;)
 	end
 
 	return mw.text.tag('table', {
 		style = [[
 			width: 200px; border-width: 1px; float: right;
 			border-style: solid; background-color: #f9f9f9;
 		]]
 	}, table.concat(content))
 end
 L.renderTaxobox = renderTaxobox
 
 
 if debug then
 	function p.debugParams(params)
 		mw.log(&quot;Start of logging params&quot;)
 		mw.log(string.rep(&quot;=&quot;, 20))
 		for k, v in pairs(params) do
 			mw.log(k, v)
 		end
 		mw.log(&quot;End of logging params&quot;)
 	end
 
 	function p.localFunction(name)
 		return L[name]
 	end
 end
 
 
 function p.taxobox(frame)
 	local config = parseConfig(frame.args)
 	local qid = frame.args.qid
 	local params = getTaxoboxParams(qid, config.count)
 	return renderTaxobox(params, frame.args)
 end
 
 
 function p.callback(frame)
 	local config = parseConfig(frame.args)
 	local qid = frame.args.qid
 	local template = frame.args.template or &quot;Taxobox&quot;
 	local params = getTaxoboxParams(qid, config.count)
 	return callbackTaxobox(template, params, frame.args, config.dryrun)
 end