# 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'  
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 = '<span>'..val..'</span>'  
end  
return returnVal  
end  

local function createRow( id, label, rawValue, link, withUid )  
if link then  
local outStr = '*<span>'..label..' <span'  
if withUid then outStr = outStr..'' end  
return outStr..'>'..link..'</span></span>\n'  
else  
return '* '..mw.text.tag('span', {}, '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  
local resolveEntity = require( 'Module:ResolveEntityId' )  
local parentArgs = copyTable(: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 --renamed 'using' -> 'with'  
'', --[10] placeholder for  
'', --[11] placeholder for  
'', --[12] placeholder for  
'', --[13] placeholder for  
'', --[14] placeholder for --renamed 'using' -> 'with'  
'', --[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 --renamed '40+' -> '40–44'  
'', --[26] placeholder for --new  
}  
local acceptableInstanceOf_Strict = {  
['Q16521'] = 'taxon', --strict  
['Q310890'] = 'monotypic taxon', --strict  
['Q98961713'] = 'extinct taxon', --strict  
['Q2568288'] = 'ichnotaxon', --strict  
['Q23038290'] = 'fossil taxon', --strict  
['Q47487597'] = 'monotypic fossil taxon', --strict  
}  
local acceptableInstanceOf_All = {  
['Q16521'] = 'taxon', --strict  
['Q310890'] = 'monotypic taxon', --strict  
['Q98961713'] = 'extinct taxon', --strict  
['Q2568288'] = 'ichnotaxon', --strict  
['Q23038290'] = 'fossil taxon', --strict  
['Q47487597'] = 'monotypic fossil taxon', --strict  
['Q42621'] = 'hybrid', --lax  
['Q235536'] = 'incertae sedis', --lax  
['Q713623'] = 'clade', --lax  
['Q848328'] = 'serotype', --lax  
['Q857968'] = 'candidatus', --lax  
['Q17487588'] = 'unavailable combination', --lax  
}  

--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( :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' 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 <code>%s</code>. Please correct %s or consider adding %s to Wikidata.',  
plural,  
table.concat(unknownParams, '</code>, <code>'),  
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 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[17] = ''  
break  
end  
end  
end  
end  
if parentMonoGenus == nil or tFroms[parentMonoGenus] == nil then  
tCats[18] = ''  
break  
end  
elseif taxonRank and taxonRank == 'Q34740' then --genus  
--TODO:  
--is monotypic genus; add species (no way to easily find these (yet?))  
--...  
end  

end  
end --for  
end --if currentItem  

--Setup navbox  
local navboxParams = {  
name = 'Taxonbar',  
body,  
list,  
group,  
}  

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', {}, '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 ) )  
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[26] = ''  
elseif sourceCount >= 40 then tCats[25] = '' --endashes  
elseif sourceCount >= 35 then tCats[24] = ''  
elseif sourceCount >= 30 then tCats[23] = ''  
elseif sourceCount >= 25 then tCats[22] = ''  
elseif sourceCount >= 20 then tCats[21] = ''  
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[19] = ''  
else  
tCats[20] = ''  
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'] = 'உயிரின வகைப்பாட்டு  
இனங்காட்டிகள்'  
if rowCount >= 4 then  
navboxParams['navbar'] = 'plain'  
else  
navboxParams['state'] = 'off'  
navboxParams['navbar'] = 'off'  
end  
elseif parentArgs['noTitle'..firstRow] then  
navboxParams['group'..firstRow] = 'உயிரின வகைப்பாட்டு  
இனங்காட்டிகள்'  
else  
navboxParams['group'..firstRow] = 'உயிரின வகைப்பாட்டு  
இனங்காட்டிகள்  
'..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  

return p