Module:Taxonbar

require('strict')

local conf = require( 'Module:Taxonbar/conf' ) --configuration module local TaxonItalics = require( 'Module:TaxonItalics' ) --use a function to conditionally italicize taxon names

--========================================================================== --                            Local functions --==========================================================================

local function isNilOrEmpty( thing ) if thing == nil or thing == '' then return true end return nil end

local function getIdFromWikidata( item, property ) local id = nil if property == 'PWikispecies:$1' then local siteLinks = item.sitelinks if siteLinks then local speciesWiki = item.sitelinks.specieswiki if speciesWiki then id = speciesWiki.title end end return id	elseif item.claims[property] == nil then return id	end -- this code picks up deprecated values on wikidata; better to use getBestStatements	for _, statement in pairs( item.claims[property] ) do		if statement.mainsnak.datavalue then			id = statement.mainsnak.datavalue.value			break		end	end local statements = item:getBestStatements(property)[1] if statements and statements.mainsnak and statements.mainsnak.datavalue and statements.mainsnak.datavalue.value then id = statements.mainsnak.datavalue.value end

return id end

local function getLink( property, db, val ) local link, returnVal = '', {} returnVal.isError = false if mw.ustring.find( val, '//' ) then link = val else if type(property) == 'number' and property > 0 then local entityObject = mw.wikibase.getEntity('P'..property) local dataType if entityObject then dataType = entityObject.datatype else returnVal.isError = true end if dataType == 'external-id' then local formatterURL = nil if property == 3746 or --Wildflowers of Israel property == 3795 or --Flora of Israel Online property == 5397   --Tierstimmenarchiv then formatterURL = entityObject:getBestStatements('P1630')[2] --use 2nd formatterURL for English version end if formatterURL == nil then formatterURL = entityObject:getBestStatements('P1630')[1] end --default to [1] if formatterURL then if formatterURL.mainsnak.datavalue and formatterURL.mainsnak.datavalue.value then --nil check for ABA link = formatterURL.mainsnak.datavalue.value end end if db == 'bow' then -- for birds of world which uses eBird identifier link = 'https://birdsoftheworld.org/bow/species/$1' elseif db == 'fossilworks' then link = 'https://paleobiodb.org/classic/basicTaxonInfo?taxon_no=$1' end elseif dataType == 'url' then local subjectItem = entityObject:getBestStatements('P1629')[1] if subjectItem then local officialWebsite = mw.wikibase.getEntity(subjectItem.mainsnak.datavalue.value.id):getBestStatements('P856')[1] if officialWebsite then	link = officialWebsite.mainsnak.datavalue.value end end elseif dataType == 'string' then local formatterURL = entityObject:getBestStatements('P1630')[1] if formatterURL then link = formatterURL.mainsnak.datavalue.value else local subjectItem = entityObject:getBestStatements('P1629')[1] if subjectItem then local officialWebsite = mw.wikibase.getEntity(subjectItem.mainsnak.datavalue.value.id):getBestStatements('P856')[1] if officialWebsite then	link = officialWebsite.mainsnak.datavalue.value end end end else returnVal.isError = true end elseif type(property) == 'string' then link = property end --local valurl = val local valurl = mw.uri.encode( val, 'PATH' ) valurl = string.gsub (valurl, '%%2F', '/') --escape '/' (e.g. issue with P5354); see wikidata T128078 and https://gerrit.wikimedia.org/r/c/mediawiki/extensions/Wikibase/+/664820/3/lib/includes/PropertyInfoSnakUrlExpander.php if type(property) == 'number' then --doublecheck language for Wildflowers of Israel ID			if property == 3746 then link = mw.ustring.gsub(link, '/hebrew/', '/english/') end --format spaces in PfaF binomials, e.g. "Elaeagnus x ebbingei" if property == 4301 then valurl = mw.ustring.gsub(valurl, '%%20', '+') end end valurl = mw.ustring.gsub(valurl,'%%','%%%%') link = mw.ustring.gsub(link, '$1', valurl) end link = mw.ustring.gsub(link, '^[Hh][Tt][Tt][Pp]([Ss]?)&#58;//', 'http%1://') --fix wikidata URL val = mw.ustring.match(val, '([^=/]*)/?$') --get display name from end of URL if mw.ustring.find( link, '//' ) then returnVal.text = '['..link..' '..mw.text.encode(mw.uri.decode(val, 'PATH'),'%[%]')..']' elseif link == '' then returnVal.text = val else returnVal.text = ' '..val..' ' end return returnVal end

local function createRow( id, label, rawValue, link, withUid ) if link then local outStr = '* '..label..' '..link..' \n' else return '* '..mw.text.tag('span', {class='error'}, 'The identifier '..id..' '..rawValue..' is not valid.')..'\n' end end

local function copyTable(inTable) if type(inTable) ~= 'table' then return inTable end local outTable = setmetatable({}, getmetatable(inTable)) for key, value in pairs (inTable) do outTable[copyTable(key)] = copyTable(value) end return outTable end

local p = {}

--========================================================================== --                                  Main --==========================================================================

function p.authorityControlTaxon( frame ) local resolveEntity = require( 'Module:ResolveEntityId' ) local whitelist = require( 'Module:Taxonbar/whitelist' ).whitelist --to create acceptableInstanceOf_Strict & acceptableInstanceOf_All local parentArgs = copyTable(frame:getParent.args) local currentTitle = mw.title.getCurrentTitle local currentEntityId = mw.wikibase.getEntityIdForCurrentPage local stringArgs = false local fromTitleCount, firstRow, rowCount = 1, 0, 0 local outString, errors = ,  local iFroms = 0 --integer size of tFroms, b/c Lua local tFroms = {} --non-sequential table of unique froms local tCats = { ,		,		'', -- [3] placeholder for '', -- [4] placeholder for '', -- [5] placeholder for '', -- [6] placeholder for '', -- [7] placeholder for '', -- [8] placeholder for '', -- [9] placeholder for '', --[10] placeholder for '', --[11] placeholder for '', --[12] placeholder for '', --[13] placeholder for '', --[14] placeholder for '', --[15] placeholder for '', --[16] placeholder for '', --[17] placeholder for '', --[18] placeholder for '', --[19] placeholder for '', --[20] placeholder for '', --[21] placeholder for '', --[22] placeholder for '', --[23] placeholder for '', --[24] placeholder for '', --[25] placeholder for '', --[26] placeholder for '', --[27] placeholder for '', --[28] placeholder for }	local acceptableInstanceOf_Strict = whitelist{ args = { 'strict' } } local acceptableInstanceOf_All = whitelist{ args = { 'all' } } --Assess the page's relationship with Wikidata local currentItem = nil if currentTitle.namespace == 10 then --i.e. Module:Taxonbar/sandbox, Template:Taxonbar/doc, etc.		if resolveEntity._id(parentArgs['from']) then currentItem = mw.wikibase.getEntity(parentArgs['from']) end if currentItem == nil then if resolveEntity._id(parentArgs['from1']) then currentItem = mw.wikibase.getEntity(parentArgs['from1']) end end elseif resolveEntity._id(currentEntityId) then currentItem = mw.wikibase.getEntity(currentEntityId) else --currentEntityId == nil/unresolvable tCats[3] = '' end if currentItem then tCats[4] = '' --unset if acceptable found for _, instanceOfState in pairs ( currentItem:getBestStatements('P31') ) do --instance of			local instanceOf = instanceOfState.mainsnak.datavalue.value.id			if acceptableInstanceOf_All[instanceOf] then tCats[4] = '' break end end end --Cleanup args for k, v in pairs( frame:getParent.args ) do		if type(k) == 'string' then --make args case insensitive local lowerk = mw.ustring.lower(k) if isNilOrEmpty( parentArgs[lowerk] ) then parentArgs[k] = nil parentArgs[lowerk] = v			end --remap abc to abc1 if mw.ustring.find(lowerk,'%d$') == nil then --if no number at end of param if isNilOrEmpty( parentArgs[lowerk..'1'] ) then parentArgs[lowerk] = nil lowerk = lowerk..'1' parentArgs[lowerk] = v				end end if v and v ~= '' then --remap 'for' to 'title' if mw.ustring.sub(lowerk,1,3) == 'for' then local forTitle = mw.ustring.gsub(lowerk,'^for','title',1) if isNilOrEmpty( parentArgs[forTitle] ) then parentArgs[lowerk] = nil lowerk = forTitle parentArgs[lowerk] = v					end end --find highest from or title param if mw.ustring.sub(lowerk,1,4) == 'from' then local fromNumber = tonumber(mw.ustring.sub(lowerk,5,-1)) if fromNumber and fromNumber >= fromTitleCount then fromTitleCount = fromNumber end --look for duplicate froms while we're here if mw.ustring.find(v, '^Q%d') then if tFroms[v] then tCats[6] = '' tFroms[v] = tFroms[v] + 1 else tFroms[v] = 1 iFroms = iFroms + 1 end if iFroms == 2 then tCats[14] = '' end end elseif mw.ustring.sub(lowerk,1,5) == 'title' then local titleNumber = tonumber(mw.ustring.sub(lowerk,4,-1)) if titleNumber and titleNumber >= fromTitleCount then fromTitleCount = titleNumber end elseif mw.ustring.lower(v) ~= 'no' and mw.ustring.lower(v) ~= 'yes' then stringArgs = true tCats[9] = '' end end end --if type(k) == 'string' end --for --Check for unknown parameters --create knowns list local acceptableArgs = { from = true, } --master list of l/c acceptable args for _, d in pairs( conf.databases ) do		if d[1] ~= 'Wikidata' then --made obsolete by from acceptableArgs[mw.ustring.lower(d[1])] = true end end for _, a in pairs( conf.aliases ) do		acceptableArgs[mw.ustring.lower(a[1])] = true end --create trimmed parents list local baseParentArgs = {} --condensed list of l/c parent args w/o trailing #s for k, _ in pairs( parentArgs ) do		if type(k) == 'string' then local lowerk = mw.ustring.lower(k) local base = mw.ustring.gsub(lowerk, '[%d]*$', '') baseParentArgs[base] = true elseif type(k) == 'number' then tCats[13] = '' end end --compare lists and spit out unknowns local unknownParams = {} for k, _ in pairs( baseParentArgs ) do		if acceptableArgs[k] == nil then tCats[12] = '' unknownParams[#unknownParams + 1] = k		end end --warn if unknown(s) present if #unknownParams > 0 then local plural = 's'		local itthem = 'them' if #unknownParams == 1 then plural = '' itthem = 'it' end errors = errors..require('Module:If preview')._warning({			mw.ustring.format( 'Unknown parameter%s. Please correct %s or consider adding %s to Wikidata.', plural, table.concat(unknownParams, ', '), itthem, itthem )		})	end --Append basionym to arg list, if not already provided if currentItem then local currentBasState = currentItem:getBestStatements('P566')[1] --basionym if currentBasState then local basionymId = currentBasState.mainsnak.datavalue.value.id			if basionymId and resolveEntity._id(basionymId) and tFroms[basionymId] == nil then --check that basionym is a strict instance of taxon local basionymItem = mw.wikibase.getEntity(basionymId) if basionymItem then for _, instanceOfState in pairs ( basionymItem:getBestStatements('P31') ) do --instance of						local instanceOf = instanceOfState.mainsnak.datavalue.value.id						if acceptableInstanceOf_Strict[instanceOf] then --housekeeping tFroms[basionymId] = 1 iFroms = iFroms + 1 fromTitleCount = fromTitleCount + 1 --append basionym & track parentArgs['from'..fromTitleCount] = basionymId tCats[15] = '' break end	end	end	end	end	end --Append original combination to arg list, if not already provided if currentItem then local currentOCState = currentItem:getBestStatements('P1403')[1] --original combination if currentOCState then local orcoId = currentOCState.mainsnak.datavalue.value.id			if orcoId and resolveEntity._id(orcoId) and tFroms[orcoId] == nil then --check that orco is a strict instance of taxon local orcoItem = mw.wikibase.getEntity(orcoId) if orcoItem then for _, instanceOfState in pairs ( orcoItem:getBestStatements('P31') ) do --instance of						local instanceOf = instanceOfState.mainsnak.datavalue.value.id						if acceptableInstanceOf_Strict[instanceOf] then --housekeeping tFroms[orcoId] = 1 iFroms = iFroms + 1 fromTitleCount = fromTitleCount + 1 --append orco & track parentArgs['from'..fromTitleCount] = orcoId tCats[16] = '' break end	end	end	end	end	end --Append monotypic genus/species to arg list of monotypic species/genus, if not already provided if currentItem then for _, instanceOfState in pairs ( currentItem:getBestStatements('P31') ) do --instance of			local taxonRank = nil local parentItem = nil local parentTaxon = nil local parentTaxonRank = nil local parentMonoGenus = nil --holy grail/tbd local childItem = nil local childTaxon = nil local childTaxonRank = nil local childMonoSpecies = nil --holy grail/tbd local instanceOf = instanceOfState.mainsnak.datavalue.value.id			if instanceOf and (instanceOf == 'Q310890' or instanceOf == 'Q47487597') then --monotypic/fossil taxon local taxonRankState = currentItem:getBestStatements('P105')[1] --taxon rank if taxonRankState then taxonRank = taxonRankState.mainsnak.datavalue.value.id end if taxonRank and taxonRank == 'Q7432' then --species --is monotypic species; add genus local parentTaxonState = currentItem:getBestStatements('P171')[1] --parent taxon if parentTaxonState then parentTaxon = parentTaxonState.mainsnak.datavalue.value.id end --confirm parent taxon rank == genus & monotypic if parentTaxon and resolveEntity._id(parentTaxon) then parentItem = mw.wikibase.getEntity(parentTaxon) if parentItem then local parentTaxonRankState = parentItem:getBestStatements('P105')[1] --taxon rank if parentTaxonRankState then parentTaxonRank = parentTaxonRankState.mainsnak.datavalue.value.id end if parentTaxonRank and parentTaxonRank == 'Q34740' then --parent == genus for _, parentInstanceOfState in pairs ( parentItem:getBestStatements('P31') ) do --instance of									local parentInstanceOf = parentInstanceOfState.mainsnak.datavalue.value.id 									if parentInstanceOf and (parentInstanceOf == 'Q310890' or parentInstanceOf == 'Q47487597') then --monotypic/fossil taxon parentMonoGenus = parentTaxon --confirmed break end end if parentMonoGenus and tFroms[parentMonoGenus] == nil then --housekeeping tFroms[parentMonoGenus] = 1 iFroms = iFroms + 1 fromTitleCount = fromTitleCount + 1 --append monotypic genus & track parentArgs['from'..fromTitleCount] = parentMonoGenus tCats[18] = '' break end	end	end	end if parentMonoGenus == nil or tFroms[parentMonoGenus] == nil then tCats[19] = '' break end elseif taxonRank and taxonRank == 'Q34740' then --genus --is monotypic genus; add species --(https://www.wikidata.org/wiki/Wikidata:Property_proposal/child_monotypic_taxon unnecessary thanks to P427!) local childTaxonState = currentItem:getBestStatements('P427')[1] --taxonomic type if childTaxonState then childTaxon = childTaxonState.mainsnak.datavalue.value.id end --confirm child taxon rank == species & monotypic if childTaxon and resolveEntity._id(childTaxon) then childItem = mw.wikibase.getEntity(childTaxon) if childItem then local childTaxonRankState = childItem:getBestStatements('P105')[1] --taxon rank if childTaxonRankState then childTaxonRank = childTaxonRankState.mainsnak.datavalue.value.id end if childTaxonRank and childTaxonRank == 'Q7432' then --child == species for _, childInstanceOfState in pairs ( childItem:getBestStatements('P31') ) do --instance of									local childInstanceOf = childInstanceOfState.mainsnak.datavalue.value.id 									if childInstanceOf and (childInstanceOf == 'Q310890' or childInstanceOf == 'Q47487597') then --monotypic/fossil taxon childMonoSpecies = childTaxon --confirmed break end end if childMonoSpecies and tFroms[childMonoSpecies] == nil then --housekeeping tFroms[childMonoSpecies] = 1 iFroms = iFroms + 1 fromTitleCount = fromTitleCount + 1 --append monotypic species & track parentArgs['from'..fromTitleCount] = childMonoSpecies tCats[17] = '' break end	end	end	end if childMonoSpecies == nil or tFroms[childMonoSpecies] == nil then tCats[20] = '' break end end --monotype searches end --monotype handling end --for end --if currentItem --Setup navbox local navboxParams = { name = 'Taxonbar', bodyclass = 'hlist', listclass = '', groupstyle = 'text-align: left;', }	for f = 1, fromTitleCount, 1 do local elements, title = {}, nil --cleanup parameters if parentArgs['from'..f] == '' then parentArgs['from'..f] = nil end if parentArgs['title'..f] == '' then parentArgs['title'..f] = nil end --remap aliases for _, a in pairs( conf.aliases ) do			local alias, name = mw.ustring.lower(a[1]), mw.ustring.lower(a[2]) if parentArgs[alias..f] and parentArgs[name..f] == nil then parentArgs[name..f] = parentArgs[alias..f]				parentArgs[alias..f] = nil end end --Fetch Wikidata item local from = resolveEntity._id(parentArgs['from'..f]) local item = mw.wikibase.getEntity(from) local label = nil if type(item) == 'table' then local statements = item:getBestStatements('P225')[1] --taxon name if statements then local datavalue = statements.mainsnak.datavalue if datavalue then label = datavalue.value end end label = label or item:getLabel else if parentArgs['from'..f] then tCats[1] = '' tCats[5] = '' errors = errors..mw.text.tag('strong', {class='error'}, 'Error: "'..				        parentArgs['from'..f]..'" is not a valid Wikidata entity ID. ') end end if label and label ~= '' then title = mw.title.new(label) end if title == nil and parentArgs['title'..f] then title = mw.title.new(parentArgs['title'..f]) end if title == nil and f == 1 then title = currentTitle end if title then if isNilOrEmpty( parentArgs['wikidata'..f] ) and (title.namespace == 0) then if parentArgs['from'..f] then parentArgs['wikidata'..f] = parentArgs['from'..f]				elseif item then parentArgs['wikidata'..f] = item.id				end end if title.namespace == 0 or stringArgs then --only in mainspace or if manual overrides exist local sourceCount = 0 for _, params in pairs( conf.databases ) do					params[1] = mw.ustring.lower(params[1]) local propId = params[3] --Wikidata fallback if requested if (item and item.claims) and (type(propId) == 'string' or (type(propId) == 'number' and propId > 0)) then local wikidataId = getIdFromWikidata( item, 'P'..propId ) local v = parentArgs[params[1]..f]						if wikidataId then if isNilOrEmpty(v) then parentArgs[params[1]..f] = wikidataId else if v and v ~= 'no' and v ~= wikidataId then tCats[11] = '' elseif v and v == wikidataId then tCats[10] = '' end end end end if (item and item.claims) and ( (type(propId) == 'number' and propId < 0)) then local wikidataId = getIdFromWikidata( item, 'P'..-propId ) --mw.addWarning ("propId=" .. tostring(propId) .. "; wikidata=" .. tostring(wikidataId)) local v = parentArgs[params[1]..f]						if v == 'yes' then if wikidataId then parentArgs[params[1]..f] = wikidataId else parentArgs[params[1]..f] = nil -- don't want to use 'yes' as id							end end end local val = parentArgs[params[1]..f]					if val and val ~= '' and mw.ustring.lower(val) ~= 'no' then if type(propId) == 'number' then if propId < 0 then propId = -propId end --allow link if propId > 0 then --link table.insert( elements, createRow( params[1], params[2]..':', val, getLink( propId, params[1], val ).text, true ) ) if params[1] == 'fossilworks' then -- fossilworks being use to link to PBDB if (elements[#elements] == elements[#elements-1]) then -- check if identical PBDB entry already set --mw.addWarning("item has both Fossilworks and PBDB identifers:" .. val) elements[#elements] = nil end end else --propId == 0; no link table.insert( elements, createRow( params[1], params[2]..':', val, val, true ) ) end else table.insert( elements, createRow( params[1], params[2]..':', val, getLink( propId, params[1], val ).text, true ) ) end if params[1] ~= 'wikidata' and params[1] ~= 'wikispecies' then sourceCount = sourceCount + 1 end end end --for if    sourceCount >= 45 then tCats[28] = '' elseif sourceCount >= 40 then tCats[27] = '' --endashes elseif sourceCount >= 35 then tCats[26] = '' elseif sourceCount >= 30 then tCats[25] = '' elseif sourceCount >= 25 then tCats[24] = '' elseif sourceCount >= 20 then tCats[23] = '' end --Generate navbox title if sourceCount > 0 then rowCount = rowCount + 1 if firstRow == 0 then firstRow = f end --set title from wikidata if it doesn't exist if isNilOrEmpty( parentArgs['title'..f] ) then parentArgs['noTitle'..f] = true parentArgs['title'..f] = title.text end --if it exists now, set row heading to title if not isNilOrEmpty( parentArgs['title'..f] ) then navboxParams['group'..f] = TaxonItalics.italicizeTaxonName(parentArgs['title'..f], false) else navboxParams['group'..f] = '' end navboxParams['list'..f] = table.concat( elements ) elseif currentEntityId and (currentEntityId == parentArgs['from'..f] or fromTitleCount == 1) then tCats[21] = '' else tCats[22] = '' end --Categorize if not isNilOrEmpty( parentArgs['from'..f] ) then tCats[1] = '' --blank "missing from" if 'from' exists if parentArgs['from'..f] == currentEntityId then tCats[2] = '' --blank "desynced" if 'from' matches current page end end if tCats[1] ~= '' then tCats[2] = '' --cannot be "desynced" if no 'from' params end end --if title.namespace == 0 or stringArgs end --if title end --for f = 1, fromTitleCount, 1 if rowCount > 0 then local Navbox = require('Module:Navbox') if rowCount > 1 then --remove duplicates and don't bother moving page title to top local rowIDs = {} for f = 1, fromTitleCount, 1 do if not isNilOrEmpty( parentArgs['title'..f] ) then if rowIDs[parentArgs['wikidata'..f]] then --remove duplicate navboxParams['group'..f] = nil navboxParams['list'..f] = nil else rowIDs[parentArgs['wikidata'..f]] = true end end end if parentArgs['title'..2] and parentArgs['title'..2] == currentTitle.text then if currentItem and parentArgs['from'..2] == currentItem['id'] then tCats[8] = '' else tCats[7] = '' end end --adjust navbox for number of rows navboxParams['title'] = 'Taxon identifiers' if rowCount >= 4 then navboxParams['navbar'] = 'plain' else navboxParams['state'] = 'off' navboxParams['navbar'] = 'off' end elseif parentArgs['noTitle'..firstRow] then --show title & taxon for 1-row taxonbars, per talk navboxParams['title'] = 'Taxon identifiers' navboxParams['state'] = 'off' navboxParams['navbar'] = 'off' else navboxParams['group'..firstRow] = 'Taxon identifiers '..navboxParams['group'..firstRow] end --return navbox outString = Navbox._navbox(navboxParams) end --if rowCount > 0 --Add categories if string.sub(currentTitle.subpageText,1,9) == 'testcases' then parentArgs['demo'] = true end if not isNilOrEmpty( parentArgs['demo'] ) then outString = outString..mw.text.nowiki(table.concat(tCats))..' '	elseif currentTitle.namespace == 0 then outString = outString..table.concat(tCats) end return outString..errors end

function p.taxonbar(frame) return p.authorityControlTaxon( frame:newChild{title = frame:getTitle} ) end

return p