モジュール:サンドボックス/森津/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.claimsproperty == nil then
		return id
	end
	-- this code picks up deprecated values on wikidata; better to use getBestStatements
	for _, statement in pairs( item.claimsproperty ) 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, '^HhTtTtPp(Ss?)://', '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 outTablecopyTable(key) = copyTable(value) end
	return outTable
end
local p = {}
--======================================================================**
--                                   Main                                   
--======================================================================**
function p.authorityControlTaxon( frame )
	local resolveEntity = require( 'Module:ResolveEntityId' )
	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  --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
		tCats3 = 
	end
	if currentItem then
		tCats4 =  --unset if acceptable found
		for _, instanceOfState in pairs ( currentItem:getBestStatements('P31') ) do --instance of
			local instanceOf = instanceOfState.mainsnak.datavalue.value.id
			if acceptableInstanceOf_AllinstanceOf then
				tCats4 = 
				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( parentArgslowerk ) then
				parentArgsk = nil
				parentArgslowerk = v
			end
			--remap abc to abc1
			if mw.ustring.find(lowerk,'%d$') == nil then --if no number at end of param
				if isNilOrEmpty( parentArgslowerk..'1' ) then
					parentArgslowerk = nil
					lowerk = lowerk..'1'
					parentArgslowerk = 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( parentArgsforTitle ) then
						parentArgslowerk = nil
						lowerk = forTitle
						parentArgslowerk = 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 tFromsv then
							tCats6 = 
							tFromsv = tFromsv + 1
						else
							tFromsv = 1
							iFroms = iFroms + 1
						end
						if iFroms == 2 then
							tCats14 = 
						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
					tCats9 = 
				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 d1 ~= 'Wikidata' then --made obsolete by from
			acceptableArgsmw.ustring.lower(d1) = true
		end
	end
	for _, a in pairs( conf.aliases ) do
		acceptableArgsmw.ustring.lower(a1) = 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*$', )
			baseParentArgsbase = true
		elseif type(k) == 'number' then
			tCats13 = 
		end
	end
	--compare lists and spit out unknowns
	local unknownParams = {}
	for k, _ in pairs( baseParentArgs ) do
		if acceptableArgsk == nil then
			tCats12 = 
			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 %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 tFromsbasionymId == 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_StrictinstanceOf then
							--housekeeping
							tFromsbasionymId = 1
							iFroms = iFroms + 1
							fromTitleCount = fromTitleCount + 1
							--append basionym & track
							parentArgs'from'..fromTitleCount = basionymId
							tCats15 = 
							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 tFromsorcoId == 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_StrictinstanceOf then
							--housekeeping
							tFromsorcoId = 1
							iFroms = iFroms + 1
							fromTitleCount = fromTitleCount + 1
							--append orco & track
							parentArgs'from'..fromTitleCount = orcoId
							tCats16 = 
							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 tFromsparentMonoGenus == nil then
									--housekeeping
									tFromsparentMonoGenus = 1
									iFroms = iFroms + 1
									fromTitleCount = fromTitleCount + 1
									--append monotypic genus & track
									parentArgs'from'..fromTitleCount = parentMonoGenus
									tCats17 = 
									break
								end
							end
						end
					end
					if parentMonoGenus == nil or tFromsparentMonoGenus == nil then
						tCats18 = 
						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',
		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(a1), mw.ustring.lower(a2)
			if parentArgsalias..f and parentArgsname..f == nil then
				parentArgsname..f = parentArgsalias..f
				parentArgsalias..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
				tCats1 = 
				tCats5 = 
				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
					params1 = mw.ustring.lower(params1)
					local propId = params3
					--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 = parentArgsparams1..f
						if wikidataId then
							if isNilOrEmpty(v) then
								parentArgsparams1..f = wikidataId
							else
								if v and v ~= 'no' and v ~= wikidataId then
									tCats11 = 
								elseif v and v == wikidataId then
									tCats10 = 
								end
							end
						end
					end
					local val = parentArgsparams1..f
					if val and val ~=  and mw.ustring.lower(val) ~= 'no' then
						if type(propId) == 'number' then
							if propId  0 then --link
								table.insert( elements, createRow( params1, params2..':', val, getLink( propId, params1, val ).text, true ) )
							else --propId == 0; no link
								table.insert( elements, createRow( params1, params2..':', val, val, true ) )
							end
						else
							table.insert( elements, createRow( params1, params2..':', val, getLink( propId, params1, val ).text, true ) )
						end
						if params1 ~= 'wikidata' and params1 ~= 'wikispecies' then
							sourceCount = sourceCount + 1
						end
					end
				end --for
				
				if     sourceCount >= 45 then tCats26 = 
				elseif sourceCount >= 40 then tCats25 =  --endashes
				elseif sourceCount >= 35 then tCats24 = 
				elseif sourceCount >= 30 then tCats23 = 
				elseif sourceCount >= 25 then tCats22 = 
				elseif sourceCount >= 20 then tCats21 = 
				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
					tCats19 = 
				else
					tCats20 = 
				end
				
				--Categorize
				if not isNilOrEmpty( parentArgs'from'..f ) then
					tCats1 =  --blank "missing from" if 'from' exists
					if parentArgs'from'..f == currentEntityId then
						tCats2 =  --blank "desynced" if 'from' matches current page
					end
				end
				if tCats1 ~=  then
					tCats2 =  --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 rowIDsparentArgs'wikidata'..f then --remove duplicate
						navboxParams'group'..f = nil
						navboxParams'list'..f = nil
					else
						rowIDsparentArgs'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
					tCats8 = 
				else
					tCats7 = 
				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
			navboxParams'group'..firstRow = 'Taxon identifiers'
		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
return p