Module:Sandbox/PhiLiP/taxobox

Lua
CodeDiscussionLinksLink count SubpagesDocumentationTestsResultsSandboxLive code All modules

Documentation for this module may be created at Module:Sandbox/PhiLiP/taxobox/doc

Code

-- 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_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 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 iterate
-- - 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 15
	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)
	local label = i18n('no-label')
	if item then
		label = item:getLabel(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|%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 = getItemLabel(author)
				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
					-- get zoologist author citation set
					elseif targetStr(author, P_AUTHOR_ABBR_ZOOLOGY) then
						label = targetStr(author, P_AUTHOR_ABBR_ZOOLOGY)
					elseif label and label ~= i18n('no-label') then
						for word in mw.ustring.gmatch(label, "%w+") do
							label = word
						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 .. ') '
				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
		return authorsstr
	end
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 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 .. "-" .. 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(i18n(pf .. "-replaces")) do
		scientific = string.gsub(scientific, scipattern, scirepl)
	end
	return scientific
end
L.formatScientificName = formatScientificName


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


local function renderTableRow(text, extra_css)
	local css = "text-align: center;"
	if extra_css then
		css = css .. " " .. 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["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) ..
		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, color)
	-- TODO: support SVG systemLanguage here
	r[#r + 1] = renderTableRow(
		"[[File:" .. params["iucn_status[image]"] ..
		"|220px|" .. params["iucn_status[label]"] .. "]]")
	return table.concat(r)
end
L.renderIUCNStatus = renderIUCNStatus


local function formatTaxon(latin, qid, scientific, vernacular, is_subject)
	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 == scientific then
		nameformat = i18n(nf .. "-with-same-vernacular-name")
	elseif vernacular then
		nameformat = i18n(nf .. "-with-diff-vernacular-name")
	else
		nameformat = i18n(nf .. "-without-vernacular-name")
	end
	local link = qid
	if _linkconfig == "sitelink" then
		link = mw.wikibase.sitelink(qid)
	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

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

		-- or according to usereferences
		if refs 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 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


local function taxonParams(qid, item, params, fetch_detail, child_detailed)
	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 is_monotypic = next(targetId(item, P_INSTANCE_OF)) == MONOTYPIC_TAXON
	local vernacular = vernacularName(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
	end

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

	if fetch_detail or (is_monotypic and child_detailed) 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)

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

	return params
end
L.taxonParams = taxonParams


-- performs the loop up the hierarchy using P_TAXON_PARENT
local function iterateRanks(
		qid, count, fetch_detail, child_detailed, 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

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

-- use arguments from second table to override the first table
-- support classical {{taxobox}} 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 frame = 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 = "{{" .. template .. "\n" ..
			table.concat(content, "\n") .. "\n}}\n"
		content = frame:callParserFunction("#tag", "nowiki", content)
		return mw.text.tag("pre", {}, 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(
			"[[File:" .. params.image .. "|220px|.]]")
	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(
			"[[File:" .. params.range_map .. "|220px|.]]")
	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("[[File:" .. params.audio .. "]]")
	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("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(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 "Taxobox"
	local params = getTaxoboxParams(qid, config.count)
	return callbackTaxobox(template, params, frame.args, config.dryrun)
end

return p