Module:Wikidata4Bio

From Wikimedia Commons, the free media repository
Jump to: navigation, search
Lua
CodeDiscussionEditHistoryNo test API DocumentationSubpagesLinksTestsResultsSandbox All modules

Module:Wikidata4Bio (talk · edit · hist · links · doc · subpages · tests / results · sandbox · all modules)


Usage
This module is used by:
Module:Wikidata4Bio/sandbox is the sandbox of this module (to test new developments) which is used in the sandbox template {{VN/sandbox}}
Functions and their usage
How to improve and test this module
  1. develop your modification in Module:Wikidata4Bio/sandbox, the sandbox of this module
  2. verify your changes in Module:Wikidata4Bio/sandbox/testcases
  3. verify your changes in {{VN}} in :
  4. verify your changes in {{ITIS}} in :
  5. report your modifications in Module:Wikidata4Bio
  6. verify your changes in Module:Wikidata4Bio/testcases
  7. verify your changes in Ailanthus altissima with normal {{VN}}
See also

Code

local _common=require('Module:Biology')

-- global variable which receives debug info if not nil (Should be =nil in Wikidata4Bio and ={} in Wikidata4Bio/sandbox)
local _debug=nil
local property_P31   = 'P31'   -- P31='instance of'
local property_P105  = 'P105'  -- P105='taxon rank'
local property_P225  = 'P225'  -- P225='taxon name'
local property_P301  = 'P301'  -- P301='category's main topic'
local property_P373  = 'P373'  -- P373='commons category'
local property_P910  = 'P910'  -- P910='topic's main category'
local property_P935  = 'P935'  -- P935='commons gallery'
local property_P1843 = 'P1843' -- P1843='taxon common name'

local property_claims_P225  = {'claims', 'P225'}  -- P225=taxon name
local property_claims_P1843 = {'claims', 'P1843'} -- P1843=taxon common name
local property_sitelinks_commonswiki_title         = {'sitelinks', 'commonswiki', 'title'}
local property_mainsnak_datavalue_value            = {'mainsnak', 'datavalue','value'}
local property_claims_P301_1_mainsnak_datavalue_value_numeric_id =  {'claims', 'P301', 1, 'mainsnak', 'datavalue', 'value', 'numeric-id'}
local property_claims_P301_2_mainsnak_datavalue_value_numeric_id =  {'claims', 'P301', 2, 'mainsnak', 'datavalue', 'value', 'numeric-id'}

-------------------- Debug utilities -------------------------------------------
-- addDebug() adds debug/verbose/trace info to _debug
function addDebug(lang, functionName, text)
	if not _debug then
		return
	end
	local index
	if lang then
		index = 'Lang ' .. lang
	else
		index = functionName .. '()'
	end
	local previousText = _debug[index]
	if previousText then
		previousText = previousText .. ', '
	else
		previousText = ''
	end
	if functionName and lang then
		previousText = previousText .. functionName .. ': ' .. text
	else
		previousText = previousText .. text
	end
	_debug[index] = previousText
end

-- getDebug() returns a formated version of _debug for the display
function getDebug()
	if not _debug then
		return ''
	end
	if tableIsEmpty(_debug) then
		return ''
	end

	-- Sort _debug into debug2
	debug2 = {}
	for key, value in pairs(_debug) do
		table.insert(debug2,{key,value})
	end
	table.sort(debug2, function(t1,t2) return t1[1] < t2[1] end)

	-- Serialize debug2 in displayedDebug
	displayedDebug = '<BR/>Debug:'
	for key, value in pairs(debug2) do
		displayedDebug = displayedDebug .. '<BR/>- ' .. value[1] .. ': ' .. mw.text.nowiki(value[2])
	end
	-- Clear debug:
	_debug={}
	return displayedDebug
end


-------------------- String utilities ------------------------------------------
-- getTextAsAsciiCode('Paphiopedilum') returns '112.104.105.111.112.101.100.105.108.117.109'
function getTextAsAsciiCode(stri)
	local result = ''
	for i = 1, string.len(stri) do
		result = result .. '.' .. string.byte(string.sub(stri, i, i))
	end
	return result
end

-- returns true if an item of listShortString in contained in longString
function stringContainsAnItemOfList(longString, listShortString)
	longString = string.lower(longString)
	for key, value in pairs(listShortString) do
		local listItem = string.lower(value)
		if string.contains(longString, listItem) then
			addDebug(nil, 'stringContainsAnItemOfList', 'longString= ' .. longString .. ' listShortString=' .. tableToString(listShortString,false) .. ' return true')
			return true
		end
	end
	addDebug(nil, 'stringContainsAnItemOfList', 'longString= ' .. longString .. ' listShortString=' .. tableToString(listShortString,false) .. ' return false')
	return false
end

-- returns true is searchedString is found in listString
function listContainsExactString(searchedString, listString)
	searchedString = string.lower(searchedString)
	for key, value in pairs(listString) do
		local listItem = string.lower(value)
		if searchedString == listItem then
			addDebug(nil, 'listContainsExactString', 'searchedString=' .. searchedString .. ' listString=' .. tableToString(listString,false) .. ' return true')
			return true
		end
	end
	addDebug(nil, 'listContainsExactString', 'searchedString=' .. searchedString .. ' listString=' .. tableToString(listString,false) .. ' return false')
	return false
end


-------------------- Table utilities -------------------------------------------
-- tableIsEmpty return true if the parameter mytable is nil or empty
function tableIsEmpty(mytable)
	if not mytable then
		return true
	end
	for key, value in pairs(mytable) do
		return false
	end
	return true
end

-- dumpPath() is an advanced tostring(). It is recursive and displays 1 ligne per significant value.
-- For exemple dumpPath('entity', mw.wikibase.getEntity()) will generate:
--   entity.type=string:item
--   entity.sitelinks.nlwiki.title=string:Nachtorchis
--   entity.sitelinks.nlwiki.site=string:nlwiki
--   entity.sitelinks.plwiki.title=string:Podkolan
--   entity.sitelinks.plwiki.site=string:plwiki
--   ...
--   entity.descriptions.es.language=string:es
--   entity.descriptions.es.value=string:'género de plantas de la familia Orchidaceae'
--   entity.descriptions.de.language=string:de
--   entity.descriptions.de.value=string:'Gattung der Familie der Orchideen (Orchidaceae)'
--   ...
--   entity.id=string:Q161849
--   entity.claims.p910[0].mainsnak.snaktype=string:value
--   entity.claims.p910[0].mainsnak.property=string:P910
--   entity.claims.p910[0].mainsnak.datavalue.value.numeric-id=number:8765698
--   entity.claims.p910[0].mainsnak.datavalue.value.entity-type=string:item
--   entity.claims.p910[0].mainsnak.datavalue.type=string:wikibase-entityid
--   entity.claims.p910[0].type=string:statement
--   entity.claims.p910[0].id=string:Q161849$10B4A9CE-CF8C-4D2D-A0E0-AE494A71EBE1
function dumpPath(path, value)
	if not value then
		return path .. '=nil'
	end
	local valueType = type(value)
	local valueStr = path .. '=' .. valueType .. ':'
	if valueType == 'table' then
		valueStr = ''
		for key, subvalue in pairs(value) do
			if string.len(valueStr) < 60000 and value ~= subvalue then
				if valueStr ~= '' then
					valueStr = valueStr .. '<BR/>'
				end
				if (type(key) == 'number') then
					valueStr = valueStr .. dumpPath(path .. '[' .. key .. ']',subvalue)
				else
					valueStr = valueStr .. dumpPath(path .. '.' .. key,subvalue)
				end
			end
		end
	else
		return valueStr .. dumpValue(value,false)
	end
	return valueStr
end

function dumpValue(value, withType)
	if not value then
		return 'nil'
	end
	local valueType = type(value)
	local valueStr = ''
	if withType then
		valueStr = valueType .. ':'
	end
	if valueType == 'table' then
	elseif valueType == 'string' then
		if string.find(value, ' ', 1, true) then
			valueStr = valueStr .. "'" .. value .. "'"
		else
			valueStr = valueStr .. value
		end
	elseif valueType == 'number' then
		return valueStr .. value
	elseif valueType == 'boolean' then
		return valueStr .. tostring(value)
	end
	return valueStr
end

function hasSmallerKeyThanFunctor(pair1, pair2)
	local key1 = pair1[1]
	if (type(key1) ~= 'string') then
		return false
	end
	local key2 = pair2[1]
	if (type(key2) ~= 'string') then
		return false
	end
	return key1 < key2
end

-- tableToString() returns a string out of a table for debug and non regression purpose sorted by keys
-- If withKey then format '<key>=<value>' is returned else format '<value>' is returned
function tableToString(mytable, withKey)
	-- Sort mytable into mytable2
	mytable2 = {}
	for key, value in pairs(mytable) do
		table.insert(mytable2,{key,value})
	end
	table.sort(mytable2, hasSmallerKeyThanFunctor)
	
	-- Serialize mytable2 into result
	local result = nil
	for key, value in pairs(mytable2) do
		local currentResult = value[2]
		if withKey then
			currentResult = value[1] .. '=' .. currentResult
		end
		if result then
			result = result .. ', ' .. currentResult
		else
			result = currentResult
		end
	end
	if not result then
		return ''
	end
	return result
end


-------------------- Namespace/articleName utilities ---------------------------

-- suppressDisambiguation() returns "Platanthera" out of "Platanthera (genus)"
function suppressDisambiguation(name)
	name = string.gsub(name, '_', ' ')
	local start = string.find(name, '(', 1, true)
	if not start then
		return name
	end
	local endp = string.find(name, ')', start, true)
	if not endp then
		return name
	end
	local part1 = mw.text.trim(string.sub(name, 1, start-1))
	local part2 = mw.text.trim(string.sub(name, endp+1))
	if string.len(part1) == 0 then
		return part2
	elseif string.len(part2) == 0 then
		return part1
	else
		return part1 .. ' ' .. part2
	end
end

-- isLink(name) return true when name is a link syntax like '[[sdfs|dfsfsd]]'
function isLink(name)
	if not name then
		return false
	end
	if string.find(name, ']]', 1, true) then
		return true
	end
	if string.find(name, '[http:', 1, true) then
		return true
	end
	if string.find(name, '[https:', 1, true) then
		return true
	end
	if string.find(name, '<ref', 1, true) then
		return true
	end
	if string.find(name,'[[', 1, true) then
		return true
	end
	local cleanedName = mw.text.killMarkers(name)
	if cleanedName ~= name then
		-- name contains markers
		return true
	end
	return false
end


-------------------- Lang utilities --------------------------------------------
local _defaultLangCode = mw.language.getContentLanguage():getCode()

function hasSmallerLangCodeThanFunctor(langCode1, langCode2)
	if langCode1 == _defaultLangCode then
		return true
	end
	if langCode2 == _defaultLangCode then
		return false
	end
	return langCode1 < langCode2
end

_languageNamesByCode = nil
_additionalLanguageNamesByCode = nil
_languageCodes = nil
-- Fill _languageNamesByCode and _languageCodes
function calcLanguages(userLangCode)
	if _languageNamesByCode then
		return
	end
	_languageNamesByCode = mw.language.fetchLanguageNames()
	_languageCodes = {}
	for langCode, _ in pairs(_languageNamesByCode) do
		table.insert(_languageCodes, langCode)
	end
	_additionalLanguageNamesByCode = mw.language.fetchLanguageNames(userLangCode,'all')
	for langCode, langName in pairs(_additionalLanguageNamesByCode) do
		if _languageNamesByCode[langCode] then
			-- standard language
		else
			table.insert(_languageCodes, langCode)
		end
	end
	
	table.sort(_languageCodes, hasSmallerLangCodeThanFunctor)
end

-- getLanguagesManagedByVN() is called by VN/doc to display all languageCodes managed by VN.
-- It does not use calcLanguages()
-- We support 417 wikipedia languages + 302 additional languages (like 'en-us')
function getLanguagesManagedByVN(args)
	local userLangCode = args.lang
	local languageNamesByCode = mw.language.fetchLanguageNames(userLangCode)

	local languageCodes = {}
	for lang, _ in pairs(languageNamesByCode) do
		table.insert(languageCodes, lang)
	end
	table.sort(languageCodes, hasSmallerLangCodeThanFunctor)

	local languages = nil
	for _, langCode in pairs(languageCodes) do
		if languages then
			languages = languages .. ', '
		else
			languages = "'''" .. tostring(table.getn(languageCodes)) .. " languages (having interwiki and wikidata): '''"
		end
		languages = languages .. langCode .. ':' .. languageNamesByCode[langCode]
	end

--	Additional Languages from https://www.mediawiki.org/wiki/Extension:Cldr are not well managed by wikicommons.
--	You cannot retrieve all their autonym.
--	fetchLanguageNames(nil,'all') returns the same as fetchLanguageNames('en')
--	See https://www.mediawiki.org/wiki/Extension_talk:Scribunto/Lua_reference_manual#fetchLanguageNames
--	And https://doc.wikimedia.org/mediawiki-core/1.25.5/php/Language_8php_source.html  search for 'TODO: also include when'
--	And https://doc.wikimedia.org/mediawiki-core/REL1_25/php/Language_8php_source.html search for 'TODO: also include when'
	local languageNamesByCode2 = mw.language.fetchLanguageNames(userLangCode,'all')

	languageCodes = {}
	for lang, _ in pairs(languageNamesByCode2) do
		if not languageNamesByCode[lang] then
			table.insert(languageCodes, lang)
		end
	end
	table.sort(languageCodes, hasSmallerLangCodeThanFunctor)

	local first = true
	for _, langCode in pairs(languageCodes) do
		if first then
			languages = languages .. "\n\n:'''And " .. tostring(table.getn(languageCodes)) .. " additional languages (without interwiki and wikidata): '''"
			first = false
		else
			languages = languages .. ', '
		end
		if langCode == 'sms' then
			-- sms: is interpreted by wikimedia as an url leading to an unwanted blue link
			languages = languages .. mw.text.nowiki('sms:')
		else
			languages = languages .. langCode .. ':'
		end
		languages = languages .. languageNamesByCode2[langCode]
	end

	-- Determine if fetchLanguageNames(nil,'all') is buggy 
	local fetchLanguageNamesNilAllBug = true
	local languageNamesByCode3 = mw.language.fetchLanguageNames(nil,'all')
	for lang, _ in pairs(languageNamesByCode3) do
		if not languageNamesByCode[lang] then
			fetchLanguageNamesNilAllBug = false	-- No more buggy ???
			break
		end
	end

	local userLangName = tostring(languageNamesByCode[userLangCode])
	languages = languages .. "\n:'''Note: additional languages name are in your language (" .. userLangName

	if fetchLanguageNamesNilAllBug then
		languages = languages .. ") because of a current limitation on additional languages'''"
	else
		languages = languages .. ") but only very temporary'''"
	end

	return languages
end


-------------------- Entities utilities ----------------------------------------

-- areSameValidEntity() return true if both entities are set and have the same id
function areSameValidEntity(entity1, entity2)
	if not entity1 then
		return false
	end
	if not entity2 then
		return false
	end
	return (entity1.id == entity2.id)
end

function createEmptyEntities(paramEntity)
	return {entity=paramEntity, otherEntity=nil, useWikidata=false, useWikidataIsCalculated=false}
end

-- retrieveEntitiesSimple() returns {entity, otherEntity}
-- Called      by retrieveEntities() which is called by getVN() and getVNTitle()
-- Also called by compareSiteIdWithWikidata() (were the is no useWikidata=)
function retrieveEntitiesSimple(entities)
	if entities == nil then
		entities = createEmptyEntities(nil)
	end
	if not mw.wikibase then
		-- Case 1
		return entities
	end
	entities.entity = mw.wikibase.getEntityObject()
	if entities.entity then
		local P301values = getNumericIdProperties(entities.entity, property_P301, 'Q')
		addDebug(nil, 'retrieveEntitiesSimple', 'P301=' .. mw.text.jsonEncode(P301values))
		if P301values.size > 1 then
 			-- Error message will be displayed by checkEntities except if entities.entity.id is in  _catItemsWithMultipleSubjects
			addDebug(nil, 'retrieveEntitiesSimple', 'multiple otherEntity (P301) found => none is used')
		elseif P301values.size == 1 then
			entities.otherEntity = mw.wikibase.getEntityObject(P301values[1])
			if entities.otherEntity then
				if areSameValidEntity(entities.otherEntity, entities.entity) then
					addDebug(nil, 'retrieveEntitiesSimple', 'otherEntity == entity')
					entities.otherEntity = nil
				else
					addDebug(nil, 'retrieveEntitiesSimple', 'otherEntity found')
				end
			else
				addDebug(nil, 'retrieveEntitiesSimple', 'otherEntity=nil when otherEntityId=' .. P301values[1])
			end
		end
	end

	return entities
end

-- retrieveEntities() returns {entity, otherEntity, useWikidata, useWikidataIsCalculated}
function retrieveEntities(args)
	local entities = createEmptyEntities(nil)
	if not mw.wikibase then
		-- Case 1
		return entities
	end
	-- wikidata library is enabled
 	if string.startsWith(args.useWikidata,'Q') then
 		-- useWikidata=Q1246
 		local success, myentity = pcall(mw.wikibase.getEntityObject, args.useWikidata)
		if not success or not myentity then
			-- Case 2
			addDebug(nil, 'retrieveEntities', 'Case2: useWikidata=' .. args.useWikidata .. ' but failed to load ' .. args.useWikidata)
			return
		end
		-- Case 3
		entities.entity = myentity
		entities.useWikidata = true
		if areSameValidEntity(entities.entity, mw.wikibase.getEntityObject()) then
			addDebug(nil, 'retrieveEntities', 'Case 3bis: useWikidata=' .. args.useWikidata .. ' but not needed as associated to the same item => useWikidata=true')
		else
			addDebug(nil, 'retrieveEntities', 'Case 3: useWikidata=' .. args.useWikidata .. ' => useWikidata=true')
		end
		entities.entitySpecified = true
 	else
 	    retrieveEntitiesSimple(entities)
		if entities.entity then
			if string.isNilOrEmpty(args.useWikidata) then
				entities.useWikidataIsCalculated = true
				local sciname = string.trimOrNullify(args.sciname)
				if sciname then
					-- Case 4.true: useWikidata set to true
					-- Case 4.false: useWikidata set to false
					-- isScientificName(sciname) returns true if sciname==<category or gallery name>
					entities.useWikidata = isScientificName(entities, sciname)
					addDebug(nil, 'retrieveEntities', 'Case 4: useWikidata not set but sciname=' .. sciname .. ' => using useWikidata=isScientificName(sciname)=' .. tostring(entities.useWikidata))
				else
					-- Case 5.true: useWikidata set to true
					-- Case 5.false: useWikidata set to false
					entities.useWikidata = isVnCalledOnlyOnce()
					addDebug(nil, 'retrieveEntities', 'Case 5: useWikidata not set and sciname not set => using useWikidata=isVnCalledOnlyOnce()=' .. tostring(entities.useWikidata))
				end
			else
				-- Case 6.true: args.useWikidata=1
				-- Case 6.false: args.useWikidata=0
				entities.useWikidata = string.isTrue(args.useWikidata)
				addDebug(nil, 'retrieveEntities', 'Case 6: args.useWikidata=' .. tostring(args.useWikidata) .. ' => entities.useWikidata=' .. tostring(entities.useWikidata))
			end
		else
			-- Case 7.nil: args.useWikidata=nil but entity=nil => we avoid useWikidataIsCalculated
			-- Case 7.true: args.useWikidata=1 but entity=nil
			-- Case 7.false: args.useWikidata=0 and entity=nil
			entities.useWikidata = false
			addDebug(nil, 'retrieveEntities', 'Case 7: args.useWikidata=' .. tostring(args.useWikidata) .. ' but as entity=nil => entities.useWikidata=' .. tostring(entities.useWikidata))
		end
	end

	return entities
end


-------------------- Wikidata properties utilities -----------------------------
-- getProperty(entity, true, {'claims','P301',1,'mainsnak','datavalue','value','numeric-id'}) does the same as
-- entity.claims.P301[0].mainsnak.datavalue.value.numeric-id except that it will never generate an error.
-- if verbose, then a debug will be added when the property has a nil value
-- Could be replaced by:
--    s = entity:getBestStatements( property_P935 )  -- no need to check if return is nil, worst case it is an empty tab []
--    if s[1] and s[1].mainsnak.datavalue.value.numeric-id
function getProperty(entity, verbose, propertyPath)
	if not entity then
		return nil
	end
	local property = entity
	local currentPath = 'entity'
	for index, propertyPathItem in pairs(propertyPath) do
		property = property[propertyPathItem]
		if verbose then
			if (type(propertyPathItem) == 'number') then
				currentPath = currentPath .. '[' .. propertyPathItem .. ']'
			else
				currentPath = currentPath .. '.' .. propertyPathItem
			end
		end
		if not property then
			if verbose then
				addDebug(nil, 'getProperty', currentPath .. '=nil')
			end
			return nil
		end
	end

	return property
end

-- getStringProperties() returns a list in the form {size=3, 1=<value>, 2=<value> ...} corresponding to <entity>.claims.<propertyName>.<index>.mainsnak.datavalue.value
-- getStringProperties(entity, P373) returns {size=3, 1='NameOfCommonsCategory', 2='Name2OfCommonsCategory' ...}
function getStringProperties(entity, propertyName)
	local claims = entity:getBestStatements( propertyName )  -- no need to check if return is nil, worst case it is an empty tab []
	local properties = {size=0}
	for _,claim in pairs(claims) do 
		local propertyValue = claim.mainsnak.datavalue.value
		if propertyValue then
			local propertyValueType = type(propertyValue)
			if propertyValueType == 'string' then
				properties.size = properties.size + 1
				propertyValue = mw.text.trim(propertyValue)
				properties[properties.size] = propertyValue
			else
				addDebug(nil, 'getStringProperties', 'found non string (' .. propertyValueType .. ') ' .. mw.text.jsonEncode(numericId))
			end
		else
			addDebug(nil, 'getStringProperties', 'found claim without mainsnak.datavalue.value ' .. mw.text.jsonEncode(claim))
		end
	end
	return properties
end

-- getNumericIdProperties(entity) returns a list in the form {size=3, 1=<number1>, 2=<number2> ...} corresponding to <entity>.claims.<propertyName>.<index>.mainsnak.datavalue.value.numeric-id
-- getNumericIdProperties(entity, P31, nil) returns {size=3, 1=<number1>, 2=<number2> ...}
-- getNumericIdProperties(entity, P31, 'Q') returns {size=3, 1='Q<number1>', 2='Q<number2>' ...}
function getNumericIdProperties(entity, propertyName, prefixToAdd)
	local claims = entity:getBestStatements( propertyName )  -- no need to check if return is nil, worst case it is an empty tab []
	local properties = {size=0}
	for _,claim in pairs(claims) do 
		local numericId = claim.mainsnak.datavalue.value['numeric-id']
		if numericId then
			local numericIdType = type(numericId)
			if numericIdType == 'number' then
				properties.size = properties.size + 1
				if prefixToAdd then
					numericId = prefixToAdd .. numericId
				end
				properties[properties.size] = numericId
			else
				addDebug(nil, 'getNumericIdProperties', 'found non number (' .. numericIdType .. ') ' .. mw.text.jsonEncode(numericId))
			end
		else
			addDebug(nil, 'getNumericIdProperties', 'found claim without mainsnak.datavalue.value.numeric-id ' .. mw.text.jsonEncode(claim))
		end
	end
	return properties
end

-- getWikidataLinkFormat1Common('ID','B','C') returns "[[wikidata:ID|'B']] <small>(C)</small>"
-- Called only by getWikidataLinkFormat1() and getWikidataLinkFormat1ForEntity()
function getWikidataLinkFormat1Common(itemId, linkLabel, complement)
	local link = '[[wikidata:' .. itemId .. '|\'' .. linkLabel .. '\']]'
	if complement then
		link = link .. ' <small>(' .. complement .. ')</small>'
	end
	return link
end

-- getWikidataLinkFormat1(property_P301) return "[[wikidata:P301|'category's main topic']] <small>(P301)</small>"
-- getWikidataLinkFormat1('Q270') return "[[wikidata:Q270|'Nepenthaceae']] <small>(Q270)</small>"
-- Called only by checkSpecifiedEntity()/checkSingleEntity()/checkTwoEntities() to display a red message
function getWikidataLinkFormat1(itemId)
	local entity = mw.wikibase.getEntity(itemId)
	if entity then
		local label = entity:getLabel()
		if label then
			return getWikidataLinkFormat1Common(itemId, label, itemId)
		end
	end
	local defaultLabel = nil
	if itemId == property_P225 then
		defaultLabel = 'taxon name'
	elseif itemId == property_P1843 then
		defaultLabel = 'taxon common name'
	elseif itemId == property_P301 then
		defaultLabel = "category's main topic"
	elseif itemId == property_P910 then
		defaultLabel = "topic's main category"
	elseif itemId == property_P373 then
		defaultLabel = 'commons category'
	else
		return getWikidataLinkFormat1Common(itemId, itemId, nil)
	end
	return getWikidataLinkFormat1Common(itemId, defaultLabel, itemId)
end

-- getWikidataLinkFormat1ForEntity(entityQ2704296) return "[[wikidata:Q2704296|'Nepenthaceae']] <small>(Q2704296)</small>"
-- Called only by checkSpecifiedEntity()/checkSingleEntity()/checkTwoEntities() to display a red message
function getWikidataLinkFormat1ForEntity(entity)
	if entity then
		local label = entity:getLabel()
		if label then
			return getWikidataLinkFormat1Common(entity.id, label, entity.id)
		end
		return getWikidataLinkFormat1Common(entity.id, entity.id, nil)
	end
	return 'nil entity'
end

-- getWikidataLinkFormat2(entityQ2704296) return "[[wikidata:Q2704296|wikidata 'Nepenthaceae']]"
function getWikidataLinkFormat2(entity)
	if entity then
		local label = entity:getLabel()
		if label then
			return '[[wikidata:' .. entity.id .. '|wikidata \'' .. label .. '\']]'
		end
		return '[[wikidata:' .. entity.id .. '|wikidata item ' .. entity.id .. ']]'
	end
	return 'nil entity'
end

-- propertiesContain() return true if searchedPropertyValue is found in properties
function propertiesContain(properties, searchedPropertyValue)
	for index=1, properties.size, 1
	do
		if properties[index] == searchedPropertyValue then
			return true
		end
	end
	return false
end

-- addWikidataError() returns a red error text and a Category:Pages with incorrect biology template usage
function addWikidataError(error, message, sortkey)
	if not sortkey then
		sortkey = '?'
	end
	return error .. '<BR/><span class="error">Error in Wikidata: ' .. message.. '</span>[[Category:Pages with biology property incorrect on Wikidata|' .. sortkey .. ']]'
end

local _catItemsWithMultipleSubjects = {
	Q8261220  = 1,  -- Category:Arctiinae     - There are 2 Arctiinae with narrow and wide sense
	Q7149713  = 1,  -- Category:Donkeys       - There are 2 syn: Equus asinus & Equus africanus asinus
	Q7328661  = 1,  -- Category:Eudicots      - There are 2 syn: eudicots & Eudicotyledoneae
	Q6256259  = 1,  -- Category:Gynatrix      - There are 2 sujects: scientific name + vernaculare name (for sv only)
	Q18282087 = 1,  -- Category:Cleistogenes  - There are 2 syn: Cleistogenes & Kengia
	Q8763590  = 1,  -- Category:Pitohui       - There are 2 sujects: scientific name + vernaculare name (for en only)
	Q9522340  = 1,  -- Category:Ruminantia    - There are 2 sujects: scientific name + Ruminant/Rumination
	Q9675089  = 1,  -- Category:Sinningia     - There are 2 sujects: scientific name + vernaculare name (for sv only)
	Q9414408  = 1,  -- Category:Homo sapiens  - There are 2 sujects: scientific name + vernaculare name
}
local _catItemsWithDisabledTests = {
	Q1456850  = 1,  -- Category:Birds         - A mess because it is a vernaculare name
	Q8700233  = 1,  -- Category:Aves          - A mess because of Category:Birds
	Q7157802  = 1,  -- Category:Animals       - A mess because it is a vernaculare name
	Q9470370  = 1,  -- Category:Gregarines    - A mess because it has many syn
	Q5608148  = 1,  -- Category:Insects       - A mess because it is a vernaculare name
}

-- checkEntities() checks that wikidata properties are coherents
-- Called by getVN()
function checkEntities(entities)
	if not entities.entity then
		-- No wikidata link => nothing to test
		return ''
	end
	if entities.entitySpecified then
		return checkSpecifiedEntity(entities)
	end
	if not _common.isCurrentNamespaceACategoryOrAGallery() then
		return ''
	end
	if _catItemsWithDisabledTests[entities.entity.id] then
		-- No test possible
		return ''
	end
	if entities.otherEntity then
		return checkTwoEntities(entities)
	else
		return checkSingleEntity(entities)
	end
end

-- checkTaxonEntity() checks that:
-- TODO
function checkTaxonEntity(entity, error)
	if not entity then
		return error
	end
	local p31properties = getNumericIdProperties(entity, property_P31, 'Q') -- P31='instance of'
   	if p31properties.size > 0 then
   		-- 'instance of' (P31) has been set
		if propertiesContain(p31properties, 'Q4167836') then -- Q4167836=='page de catégorie de Wikimedia'
   			-- entity is like 'Category:Lissomini (Q30015898)': not much check can be done
   			return error
		elseif propertiesContain(p31properties, 'Q16521') or propertiesContain(p31properties, 'Q310890') or propertiesContain(p31properties, 'Q23038290') then -- Q16521=='Taxon' Q310890='Monotypic taxon' Q23038290='fossil taxon'
   			-- entity is like 'Lissomini (Q21222258)': a lot of tests can be done
   		else
   			-- Strange P31 value
			error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entity) .. ' property ' .. getWikidataLinkFormat1(property_P31) .. ' has a strange value ' .. getWikidataLinkFormat1(p31properties[1]) .. ' (currently accepted values: '
				.. getWikidataLinkFormat1('Q4167836') .. ', ' .. getWikidataLinkFormat1('Q16521') .. ', ' .. getWikidataLinkFormat1('Q310890') .. ' and ' .. getWikidataLinkFormat1('Q23038290') .. ')', 'C')
			return error
   		end
   	else
		error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entity) .. ' has no property ' .. getWikidataLinkFormat1(property_P31), 'C')
		return error -- no property P31, so we cannot know it it is the taxon item => no more checks
   	end

	property = entity:getBestStatements(property_P225)  -- no need to check if return is nil, worst case it is an empty tab []
   	if property[1] and property[1].mainsnak.datavalue.value then
   		-- 'taxon name' (P225) has been set
   	else
		error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entity) .. ' has no property ' .. getWikidataLinkFormat1(property_P225), 'D')
   	end

	property = entity:getBestStatements(property_P105)  -- no need to check if return is nil, worst case it is an empty tab []
   	if property[1] and property[1].mainsnak.datavalue.value then
   		-- taxon rank (P105) has been set
   	else
		error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entity) .. ' has no property ' .. getWikidataLinkFormat1(property_P105), 'E')
   end
   return error
end

-- returns '[[:Category:INPUT|INPUT]]' out of 'INPUT'
function getCategoryLink(category)
	return '[[:Category:' .. category .. '|' .. category .. ']]'
end

function checkSpecifiedEntity(specifiedEntities)
	local cleanEntities = createEmptyEntities(nil)
    retrieveEntitiesSimple(cleanEntities)

	if areSameValidEntity(specifiedEntities.entity, cleanEntities.entity) then
		if cleanEntities.otherEntity then
			addDebug(nil, 'checkSpecifiedEntity', 'specifiedEntity==cleanEntities.entity && cleanEntities.otherEntity => checkTwoEntities()')
			return checkTwoEntities(cleanEntities)
		else
			addDebug(nil, 'checkSpecifiedEntity', 'specifiedEntity==cleanEntities.entity but !cleanEntities.otherEntity => checkSingleEntity()')
			return checkSingleEntity(cleanEntities)
		end
	end
	if areSameValidEntity(specifiedEntities.entity, cleanEntities.otherEntity) then
		addDebug(nil, 'checkSpecifiedEntity', 'specifiedEntity=cleanEntities.otherEntity => checkTwoEntities()')
		return checkTwoEntities(cleanEntities)
	end
	return ''
end

-- checkSingleEntity() checks that:
-- if item is a category:
--	8) item.P373 (commons category) == currentCommonsCategory
--  a) item should not have a P910 (topic's main category)
-- if item is a gallery:
--	9) item.P935 (commons gallery) == currentCommonsGallery
--  b) item should not have a P301 (category's main topic)
function checkSingleEntity(entities)
	local currentPageName = mw.title.getCurrentTitle().text
	local error = checkTaxonEntity(entities.entity, '')
	
	if _common.isCurrentNamespaceACategory() then
		local entity_P373 = getStringProperties(entities.entity, property_P373)
		if entity_P373.size == 1 then
			if currentPageName ~= entity_P373[1] then
				error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not " .. getCategoryLink(entity_P373[1]) .. ').', '8')
			end
		elseif entity_P373.size > 1 then
			error     = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. ' should not have multiple values.', '8')
		else
			error     = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not empty).", '8')
		end
		local entity_P910 = getNumericIdProperties(entities.entity, property_P910, 'Q')
		if entity_P910.size >= 1 then
			error     = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' has ' .. getWikidataLinkFormat1(property_P910) .. '=' .. getWikidataLinkFormat1(entity_P910[1]) .. ' so you should move the link to current wikicommons category from ' .. getWikidataLinkFormat1(entities.entity.id) .. ' to ' .. getWikidataLinkFormat1(entity_P910[1]) .. '.', 'A')
		end
	else
		local entity_P935 = getStringProperties(entities.entity, property_P935)
		if entity_P935.size == 1 then
			if currentPageName ~= entity_P935[1] then
				error = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. " should be '" .. currentPageName .. "' (not [[" .. entity_P935[1] .. ']]).', '9')
			end
		elseif entity_P935.size > 1 then
			error     = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. ' should not have multiple values.', '9')
		else
			-- temporarely disabled (too much work)
			--error = addWikidataError(error,   'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. " should be '" .. currentPageName .. "' (not empty).", '9')
		end
		local entity_P301 = getNumericIdProperties(entities.entity, property_P301, 'Q')
		if entity_P301.size >= 1 then
			error     = addWikidataError(error, 'wikidata item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P301) .. '=' .. getWikidataLinkFormat1(entity_P301[1]) .. ' so you should move the link to current wikicommons gallery from ' .. getWikidataLinkFormat1(entities.entity.id) .. ' to ' .. getWikidataLinkFormat1(entity_P301[1]) .. '.', 'B')
		end
	end
	return error
end

-- checkTwoEntities() checks that:
-- 1) entities.entity.P301.size (category's main topic) == 1 (list of exceptions: _catItemsWithMultipleSubjects)
-- 2) entities.entity.P301 (category's main topic) == entities.otherEntity
-- 3) entities.otherEntity.P910.size (topic's main category) > 1
-- 4) entities.entity is included in entities.otherEntity.P910 (topic's main category)
-- 5) entities.entity.P373 (commons category) == currentPageName
-- 6) entities.otherEntity.P373 (commons category) == currentPageName
-- 7) entities.entity.P935 (commons gallery) == entities.otherEntity.P935 (commons gallery) (They can be both empty)
function checkTwoEntities(entities)
	local error = checkTaxonEntity(entities.otherEntity, '')
	local entity_P301 = getNumericIdProperties(entities.entity, property_P301, 'Q')
	if entity_P301.size == 0 then
		error     = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P301) .. ' should be ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' (not empty).', '1')
	elseif entity_P301.size > 1 then
		if _catItemsWithMultipleSubjects[entities.entity.id] then
			-- These few items have multiple subjects and it seems normal. Sadly we will not be able to extract data for the otherentity as there are 2
		else
			error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' has multiple ' .. getWikidataLinkFormat1(property_P301) .. '.', '1')
		end
	else
		if entity_P301[1] ~= entities.otherEntity.id then
			error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P301) .. ' should be ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' (not ' .. getWikidataLinkFormat1(entity_P301[1]) .. '.', '2')
		end
	end

	local otherEntity_P910 = getNumericIdProperties(entities.otherEntity, property_P910, 'Q')
	if otherEntity_P910.size == 0 then
		error     = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P910) .. ' should be ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' (not empty).', '3')
	else
		-- Multiple cat can be link to same gallery: Catégorie:Polygonaceae (Q7416468) & Catégorie:Polygonaceae (noms scientifiques) (Q9085824)
		--error = addWikidataError(error, '[[wikidata:' .. entities.otherEntity.id .. '|wikidata gallery item]] has multiple ' .. getWikidataLinkFormat1(property_P910) .. '.', '2')
		if not propertiesContain(otherEntity_P910, entities.entity.id) then
			error = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P910) .. ' should contain ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' (currently only ' .. getWikidataLinkFormat1(otherEntity_P910[1]) .. ').', '4')
		end
	end

	local currentPageName = mw.title.getCurrentTitle().text

	local entity_P373 = getStringProperties(entities.entity, property_P373)
	if entity_P373.size == 1 then
		if currentPageName ~= entity_P373[1] then
			error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not " .. getCategoryLink(entity_P373[1]) .. ').', '5')
		end
	elseif entity_P373.size > 1 then
		error     = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. ' should not have multiple values.', '5')
	else
		error     = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not empty).", '5')
	end

	local otherEntity_P373 = getStringProperties(entities.otherEntity, property_P373)
	if otherEntity_P373.size == 1 then
		if currentPageName ~= otherEntity_P373[1] then
			error = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not " .. getCategoryLink(otherEntity_P373[1]) .. ').', '6')
		end
	elseif otherEntity_P373.size > 1 then
		error     = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. ' should not have multiple values.', '6')
	else
		error     = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P373) .. " should be '" .. currentPageName .. "' (not empty).", '6')
	end

	if error ~= '' then
		-- Excellent test to be activated later
		local entity_P935 = getStringProperties(entities.entity, property_P935)
		local otherEntity_P935 = getStringProperties(entities.otherEntity, property_P935)
	
		if entity_P935.size > 1 or otherEntity_P935.size > 1 then
			if entity_P935.size > 1 then
				error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. ' should not have multiple values.', '7')
			end
			if otherEntity_P935.size > 1 then
				error = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. ' should not have multiple values.', '7')
			end
		elseif entity_P935.size == 1 then
			local galleryName = entity_P935[1]
			if otherEntity_P935.size == 1 then
				if galleryName ~= otherEntity_P935[1] then
					error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' and gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity)
						' should have the same ' .. getWikidataLinkFormat1(property_P935) .. ' (not [[' .. galleryName .. ']] and [[' .. otherEntity_P935[1] ..']]).', '7')
				end
			elseif otherEntity_P935.size == 0 then
				error = addWikidataError(error, 'wikidata gallery item ' .. getWikidataLinkFormat1ForEntity(entities.otherEntity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. " should be '" .. galleryName .. "' (not empty).", '7')
			end
		else
			-- entity_P935.size==0
			if otherEntity_P935.size == 1 then
				error = addWikidataError(error, 'wikidata cat item ' .. getWikidataLinkFormat1ForEntity(entities.entity) .. ' property ' .. getWikidataLinkFormat1(property_P935) .. " should be '" .. otherEntity_P935[1] .. "' (not empty).", '7')
			elseif otherEntity_P935.size == 0 then
				-- Both are empty
			end
		end
	end

	--local entity_P935 = getProperty(entities.entity,      property_sitelinks_commonswiki_title)
	
	return error
end

-- global variable which stores the result of getScientificNamesFromWikidata()
local _scientificNamesFromWikidata = nil

-- getScientificNamesFromWikidata() return a dictionary containing all the scientific names described by wikidata property P225
function getScientificNamesFromWikidata(entities)
	if _scientificNamesFromWikidata then
		return _scientificNamesFromWikidata
	end
	_scientificNamesFromWikidata = {}

	if not mw.wikibase then
		-- wikidata library is not enabled
		return _scientificNamesFromWikidata
	end

	if entities.entity then
		local p225 = getProperty(entities.entity, false, property_claims_P225)
		if p225 then
			for key, value in pairs(p225) do
				name = getProperty(value, false, property_mainsnak_datavalue_value)
				if name then
					name = mw.text.trim(name)
					_scientificNamesFromWikidata[name] = name
				end
			end
		end
	end
	if entities.otherEntity then
		local p225 = getProperty(entities.otherEntity, false, property_claims_P225)
		if p225 then
			for key, value in pairs(p225) do
				name = getProperty(value, false, property_mainsnak_datavalue_value)
				if name then
					name = mw.text.trim(name)
					_scientificNamesFromWikidata[name] = name
				end
			end
		end
	end

	addDebug(nil,'getScientificNamesFromWikidata',tableToString(_scientificNamesFromWikidata,false))
	return _scientificNamesFromWikidata
end

-- global variable which stores the result of getScientificNames()
_scientificNames = nil

function addScientificNameAndItsItalic(name)
	_scientificNames[name] = name
	name = "''" .. name .. "''"
	_scientificNames[name] = name
end

function addScientificName(name)
	name = string.lower(name)
	addScientificNameAndItsItalic(name)

	local spacePos = string.find(name, ' × ', 1, true)
	if spacePos then
		-- Official WikiCommons syntax: 'A × B' => let us add 'A ×B' and 'A X B'
		addScientificNameAndItsItalic(string.gsub(name, ' × ', ' ×', 2))
		addScientificNameAndItsItalic(string.gsub(name, ' × ', ' x ', 2))
	else
		spacePos = string.find(name, ' ×', 1, true)
		if spacePos then
			-- Syntax: 'A ×B' => let us add 'A × B' and 'A X B'
			addScientificNameAndItsItalic(string.gsub(name, ' ×', ' × ', 2))
			addScientificNameAndItsItalic(string.gsub(name, ' ×', ' x ', 2))
		else
			spacePos = string.find(name, ' x ', 1, true)
			if spacePos then
				-- Syntax: 'A X B' => let us add 'A ×B' and 'A × B'
				addScientificNameAndItsItalic(string.gsub(name, ' x ', ' ×', 2))
				addScientificNameAndItsItalic(string.gsub(name, ' x ', ' × ', 2))
			end
		end
	end
	--[=====[ Add GenusName out of species and subspecies.
	-- Problem 'Puma concolor' vernacular name is perhaps 'Puma' because 'Puma yagouaroundi' is named Jaguarondi
	local spacePos = string.find(name, ' ', 1, true)
	if spacePos then
		local genusName = mw.text.trim(string.sub(name, 1, spacePos-1))
		addScientificNameAndItsItalic(genusName)
	end
	--]=====]
end

-- getScientificNames() return a dictionary containing all the possible lowercase scientific names of the taxon described out of:
-- * Case 1: current category/gallery name (which is supposed to be a scientific name)
-- * Case 2: {{pagename}}
-- * Case 3: wikidata property P225 (via getScientificNamesFromWikidata())
-- * Case 1bis and 3bis: genusName out of speciesName for monotypic genus (Category:Petrobium arboreum has interwiki named Petrobium)
function getScientificNames(entities)
	if _scientificNames then
		return _scientificNames
	end
	_scientificNames = {}

	-- Case 1:
	local name = suppressDisambiguation(mw.title.getCurrentTitle().text)
	addScientificName(name)

	-- Case 2:
	addScientificName('{{pagename}}')

	-- Case 3:
	for key, value in pairs(getScientificNamesFromWikidata(entities)) do
		addScientificName(value)
	end

	addDebug(nil,'getScientificNames',tableToString(_scientificNames,false))
	return _scientificNames
end

-- isScientificName(name) return true when name == the category/gallery name, which is supposed to be the scientific name
function isScientificName(entities, name)
	if not name then
		return false
	end
	name = mw.text.trim(name)
	name = suppressDisambiguation(name)
	name = string.lower(name)
	
	getScientificNames(entities)
	
	if _scientificNames[name] then
		return true
	else
		return false
	end
end

-- getVNFromWikidataVN() returns the VernacularName from wikidata P1843 for a specific lang
function getVNFromWikidataVN(entities, entityToTest, lang)
	local claimsP1843 = getProperty(entityToTest, false, property_claims_P1843)
	if claimsP1843 then
		for index, p1843 in pairs(claimsP1843) do
			local value = getProperty(p1843, false, property_mainsnak_datavalue_value)
			if value and value.language == lang then
				if isScientificName(entities, value.text) then
					-- this gallery/category vernaculare name is in fact a scientific name, so it is a vernaculare name
					return nil
				end
				return value.text
			end
		end
	end
	return nil
end

-- getVNFromWikidataInterwiki() returns the VernacularName from wikidata interwiki for a specific lang
-- (if different from {{PAGENAME}} which is supposed to be the scientific name)
function getVNFromWikidataInterwiki(entities, lang, interwiki)
	if interwiki then
		interwiki = _common.suppressCategory(interwiki)
		if not isScientificName(entities, interwiki) then
			-- this gallery/category interwiki is not a scientific name, so it is a vernaculare name
			--addDebug(lang,nil,'Interwiki ' .. lang .. '=' .. interwiki .. ' is not a sciname')
			return interwiki
		end
		--addDebug(lang,nil,'Interwiki ' .. lang .. '=' .. interwiki .. ' rejected as it is a sciname')
	else
		--addDebug(lang,nil,'Interwiki ' .. lang .. '=nil')
	end
	return nil
end

-- getVNFromWikidataLabel() returns the VernacularName from wikidata label for a specific lang
-- (if different from {{PAGENAME}} which is supposed to be the scientific name)
function getVNFromWikidataLabel(entities, lang, entityToTest)
	local label = getProperty(entityToTest, false, {'labels', lang, 'value'})
	if label then
		label = _common.suppressCategory(label)
		if not isScientificName(entities, label) then
			-- this gallery/category label is not a scientific name, so it is a vernaculare name
			return label
		end
		--addDebug(lang,nil,'Label ' .. lang .. '=' .. label .. ' rejected as it is a sciname')
	end
	return nil
end

-- calcVNEntry() puts together the different info coming from VN parameters (lang & default) and wikidata (interwiki & vnFromWikidata)
function calcVNEntry(lang, interwiki, otherInterwiki, vnFromWikidata, vnSource, default)
	local vnEntry = vnFromWikidata
	local vnEntryDescription = vnSource
	local interwikiDebug = 'interwiki'
	if not interwiki then
		interwiki = otherInterwiki
		interwikiDebug = 'otherInterwiki'
	end
	if interwiki then
		vnEntry = '[[:' .. lang .. ':' .. interwiki .. '|' .. vnFromWikidata .. ']]'
		vnEntryDescription = 'bis: [[' .. interwikiDebug ..'|' .. vnSource .. ']]'
	else
		vnEntryDescription = ': ' .. vnEntryDescription
	end
	if default then
		if string.contains(vnFromWikidata, default) then -- string.contains(long,small)
			-- default is in vnFromWikidata => no need to display default
			-- example: default='cat' vnFomInterwiki='common cat'
			addDebug(lang,nil,'parameter ' .. lang .. ' rejected as contained in ' .. vnSource .. ', Case1' .. vnEntryDescription)
			return vnEntry
		else
			if string.contains(default, vnFromWikidata) then -- string.contains(long,small)
				-- vnFromWikidata is in default => no need to display vnFromWikidata
				-- example: default='[[:en:cat|]]' vnFomInterwiki='cat'
				if isLink(default) then
					addDebug(lang,nil,vnSource .. ' rejected as contained in VNparameter which is a link, Case2: VNparameter')
					return default
				else
					if interwiki then
						addDebug(lang,nil,vnSource .. ' rejected as contained in VNparameter, Case3: [[' .. interwikiDebug .. '|parameter]]')
						return '[[:' .. lang .. ':' .. interwiki .. '|' .. default .. ']]'
					else
						-- happens when VN is called with de= + wikidata has no 'de' interwiki + wikidata has a 'de' label + VN|de= contains 'de' label
						addDebug(lang,nil,vnSource .. ' rejected as contained in VNparameter + no interwiki, Case4: VNparameter')
						return default
					end
				end
			else
				addDebug(lang,nil,'Case5' .. vnEntryDescription .. ', VNparameter')
				return vnEntry .. ', ' .. default
			end
		end
	else
		addDebug(lang,nil,'Case6' .. vnEntryDescription)
		return vnEntry
	end
end

-- getVernacularNameFromWikidata() returns a vernacular name (often in form of a wiki link) for getVNEntry()
function getVernacularNameFromWikidata(entities, lang, default)
	if isScientificName(entities, default) then
		addDebug(lang,nil,lang .. ' parameter is a scientificName')
		default = nil
	end
  
	local interwiki      = getProperty(entities.entity,      false, {'sitelinks', lang .. 'wiki', 'title'})
	local otherInterwiki = getProperty(entities.otherEntity, false, {'sitelinks', lang .. 'wiki', 'title'})

	-- First try entity.claims.P1843.<index>.mainsnak.datavalue.value.text
	local vnFromP1843 = getVNFromWikidataVN(entities, entities.entity, lang)
	if vnFromP1843 then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFromP1843, property_P1843, default)
	end

	local vnFromOtherP1843 = getVNFromWikidataVN(entities, entities.otherEntity, lang)
	if vnFromOtherP1843 then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFromOtherP1843, 'otherP1843', default)
	end

	-- Second try entity.sitelinks.frwiki.title (interwiki)
	local vnFomInterwiki = getVNFromWikidataInterwiki(entities, lang, interwiki)
	if vnFomInterwiki then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFomInterwiki, 'interwiki', default)
	end

	local vnFomOtherInterwiki = getVNFromWikidataInterwiki(entities, lang, otherInterwiki)
	if vnFomOtherInterwiki then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFomOtherInterwiki, 'otherInterwiki', default)
	end

	-- Third try entity.labels.<lang>.value
	local vnFromLabel = getVNFromWikidataLabel(entities, lang, entities.entity)
	if vnFromLabel then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFromLabel, 'label', default)
	end

	local vnFromOtherLabel = getVNFromWikidataLabel(entities, lang, entities.otherEntity)
	if vnFromOtherLabel then
		return calcVNEntry(lang, interwiki, otherInterwiki, vnFromOtherLabel, 'otherLabel', default)
	end

	-- Interwiki and label are not provided or are scientific name
	if default and not isLink(default) then
		if interwiki then
			addDebug(lang,nil,'Case7: [[interwiki|VNparameter]]')
			if lang == 'en-us' then
				lang = 'en'
			end
			return '[[:' .. lang .. ':' .. interwiki .. '|' .. default .. ']]'
		end
		if otherInterwiki then
			addDebug(lang,nil,'Case7bis: [[otherInterwiki|VNparameter]]')
			if lang == 'en-us' then
				lang = 'en'
			end
			return '[[:' .. lang .. ':' .. otherInterwiki .. '|' .. default .. ']]'
		end
	end
	if default then
		addDebug(lang,nil,'Case8: VNparameter')
	else
		-- no need to to a trace if no interwiki, no label, no VNparameter
	end
	return default
end

-- getVNEntry() returns HTML for one language in getVN()
function getVNEntry(entities, langCode, langName, additionalLang, default, useWikidata, bold)
	local vernacularName
	if not entities.entity then
		-- This gallery/category has no wikidata element (you are perhaps in the template page)
		vernacularName = default
	elseif not useWikidata then
		-- We are required not to use wikidata
		vernacularName = default
	elseif additionalLang then
		-- additionalLang have no interwiki, no wikidata
		if default then
			addDebug(langCode,nil,'Case10: VNparameter because additionalLang')
		end
		vernacularName = default
	else
		vernacularName = getVernacularNameFromWikidata(entities, langCode, default)
	end
	if vernacularName and string.len(vernacularName) > 0 then
		-- <bdi> is just like <span> but works better when there is mixed bidi text
		local entry = "* '''" .. '<bdi lang="' .. langCode .. '">' .. langName
		if _debug then
			-- In VN/sandbox, let us display the langCode
			entry = entry .. " (" .. langCode .. ")"
		end
		entry = entry .. "</bdi>:"
		if not bold then
			-- we are already in bold (to display lang), let us close bold to display vernacularName
			entry = entry .. "'''"
		end
		entry = entry .. '&nbsp;<bdi class="vernacular" lang="' .. langCode .. '">' .. vernacularName .. "</bdi>"
		if bold then
			-- close bold if not closed before
			entry = entry .. "'''"
		end
		return entry .. '\n'
	else
		return nil
	end
end

-- findTemplate() return the position of the first call of <templateName> after startPos
function findTemplate(wikicode, templateName, templateForms ,templateFormsDebug, startPos)
	local firstPos = nil
	for index, templateForm in pairs(templateForms) do
		local currentPos = string.find(wikicode,templateForm,startPos,true)
		if currentPos then
			--addDebug(nil,'findTemplate','Found ' .. templateFormsDebug[index] .. ' at pos ' .. currentPos)
			if firstPos then
				firstPos = math.min(currentPos,firstPos)
			else
				firstPos = currentPos
			end
		end
	end
	--if firstPos then
		--addDebug(nil,'findTemplate','Finaly: Found ' .. templateName .. ' starting from ' .. startPos .. ' at pos ' .. firstPos)
	--else
		--addDebug(nil,'findTemplate','Finaly: Found no ' .. templateName .. ' starting from ' .. startPos)
	--end
	return firstPos
end

-- isTemplateCalledOnlyOnce() returns true if there are 0..1 call of <templateName> in the calling page or false if there are 2 or more
function isTemplateCalledOnlyOnce(templateName, templateForms, templateFormsDebug)
	local wikicode = mw.title.getCurrentTitle():getContent()
	if not wikicode then
		-- Called from preview before creation of page
		return false
	end
	if not templateFormsDebug then
		templateFormsDebug = templateForms
	end
	local firstPos = findTemplate(wikicode,templateName,templateForms,templateFormsDebug,1)
	if not firstPos then
		-- There is not even 1 call
		--addDebug(nil,'isTemplateCalledOnlyOnce','no ' .. templateName .. ' found (strange)')
		return false
	end
	firstPos = findTemplate(wikicode,templateName,templateForms,templateFormsDebug,firstPos+3)
	if firstPos then
		-- There is at least 2 calls
		--addDebug(nil,'isTemplateCalledOnlyOnce','multiple ' .. templateName .. ' found => return false')
		return false
	else
		-- Only one call to template
		--addDebug(nil,'isTemplateCalledOnlyOnce','single ' .. templateName .. ' found => return true')
		return true
	end
end

nvForms		 = {'{{VN|', '{{VN\n', '{{VN ', '{{VN/', '{{VN}'}
nvFormsDebug = {'{{VN|', '{{VN<cr>', '{{VN<space>', '{{VN/', '{{VN}'}

-- isVnCalledOnlyOnce() returns true if there are 0..1 VN in the calling page or false if there are 2.. VN
function isVnCalledOnlyOnce()
	return isTemplateCalledOnlyOnce('VN',nvForms,nvFormsDebug)
end

-- isEnglishName(name) tried to distinguish english name from scientific name
function isEnglishName(name)
	local lowerName = string.lower(name)
	if string.find(lowerName, 'hybrid', 1, true) then
		-- like 'Mammal hybrids' or "''Ara''-Hybride" or "''Ara'' hybrids"
		return true
	elseif string.find(lowerName, ' goat', 1, true) then
		-- Dutch white goat
		--addDebug(nil,'isEnglishName', default .. ' is in fact an goat')
		return true
	elseif string.find(lowerName, 'cultivars', 1, true) then
		-- like 'Neoreglia cultivars'
		return true
	elseif string.find(lowerName, 'fossil specimens', 1, true) then
		-- like 'Oudenodon fossil specimens'
		return true
	else
		-- Remove ' because next test is about first character.
		-- We are not interesting in testing if ' is the first param
		-- For example "''Magnolie''" was detected as an english name because M was not the first character
		name = string.gsub(name, "'", '')
		if string.upperFirstLowerOthers(name) ~= string.upperFirst(name) then
			-- like 'Waterlily Dahlias' because of the D in the middle
			return true
		end
	end
	return false
end

-- verifyVNParameter() returns nil when a VN parameter is accepted or an error category when the parameter is incorrect
function verifyVNParameter(entities, lang, default)
	if not default then
		return nil
	end

	if isScientificName(entities, default) then
		-- default is a scientific name
		-- addDebug(lang,'verifyVNParameter',default .. ' is a scientific name')
		if lang == 'la' then
			-- Correct: case where scientific name is a real latin name (Abies)
		elseif isEnglishName(default) then
			-- Correct: |en=Mammal hybrids or |en=Dutch white goat or |en=Waterlily Dahlias
			--addDebug(lang,'verifyVNParameter',default .. ' is in fact an goat')
		else
			return _common.incorrectBiologyTemplateUsage('VN', 'Parameter ' .. lang .. ' uses scientific name', 'VN')
		end
	end
	if string.find(default, "'''", 1, true) then
		-- default contains bold
		return _common.incorrectBiologyTemplateUsage('VN', 'Parameter ' .. lang .. ' uses bold', 'VN')
	end
	if string.startsWith(default, "''") then
		-- default starts with italic
		if string.find(default, "'':", 1, true) then
			-- Correct: |ja=''Vespa mandarinia japonica'': XXX
			-- Incorrect: |en='''[[:en:whatever|]]'''
		elseif isEnglishName(default) then
			-- Correct: |ja=''Oudenodon'' fossil specimens |de=''Neoreglia'' cultivars |de=''Ara''-Hybride |en=''Ara'' hybrids
		else
			-- Incorrect: |en=''Vespa mandarinia japonica''
			return _common.incorrectBiologyTemplateUsage('VN', 'Parameter ' .. lang .. ' uses italic', 'VN')
		end
	end
	return nil
end

local _excludedWikiProjects = {
	wikidatawiki  = 1,
	commonswiki   = 1,
	specieswiki   = 1,
	metawiki      = 1,
	mediawikiwiki = 1
}

-- calcAdditionalInterwiki() returns interwikis from specified entity (useWikidata=Qxxx) or otherEntity
function calcAdditionalInterwiki(entities)
    local entitySource = entities.otherEntity
    local entityAutomaticInterwiki = entities.entity
	if entities.entitySpecified then
    	entitySource = entities.entity -- specified by user
    	entityAutomaticInterwiki = mw.wikibase.getEntityObject() -- not used by VN but used by page for automaticInterwiki
	end

	local additionalInterwiki = ''
	if entitySource and entitySource.sitelinks then
		for i, j in pairs(entitySource.sitelinks) do
			local lang = mw.ustring.sub( j.site, 1, -5) -- split j.site into language and project parts
			local proj = mw.ustring.sub( j.site, -4)
			if not _excludedWikiProjects[j.site] and proj == 'wiki' then -- excludes sites on the list as well as Wikisource, Wikiquote, Wikivoyage etc
				if (entityAutomaticInterwiki and (not entityAutomaticInterwiki.sitelinks or not entityAutomaticInterwiki.sitelinks[j.site])) or not entityAutomaticInterwiki then -- excludes interwiki to projects that already have sitelinks in the present page
					lang = mw.ustring.gsub(lang, '_','-')
					additionalInterwiki = additionalInterwiki .. '[[' .. lang .. ':' .. j.title .. ']]' -- put together a interwiki-link to other projects
				end
			end
		end
	end
	addDebug(nil, 'calcAdditionalInterwiki', 'AdditionalInterwiki=' .. additionalInterwiki)
	return additionalInterwiki
end

-- getVN() is called by Template:VN
function getVN(args)
	-- Calc entity & otherEntity & useWikidata & useWikidataIsCalculated
	local entities = retrieveEntities(args)

	--addDebug(nil, 'getVN', mw.text.jsonEncode(entities))

	-- Access parameters
	local nocat = string.isTrue(args.nocat)
	local userLangCode = args.lang --frame:preprocess("{{int:Lang}}") or mw.language.getContentLanguage():getCode()
	local default = string.trimOrNullify(args[userLangCode])
	--addDebug(nil,'getVN','userLangCode=' .. tostring(userLangCode))

	-- Fill _languageNamesByCode, _additionalLanguageNamesByCode and _languageCodes
	calcLanguages(userLangCode)

	-- Calc first entry for user's language
	local vn = ''
	local additionalLang = false
	local userlangName = _languageNamesByCode[userLangCode]
	if userlangName then
		local vnEntry = getVNEntry(entities, userLangCode, userlangName, additionalLang, default, entities.useWikidata, true)
		if vnEntry then
			vn = vnEntry
		end
	end

	-- Calc entries for all other languages
	local additionalCategory = ''
	for _, langCode in pairs(_languageCodes) do
	 	default = args[langCode]
	 	if not nocat then
	 		local verification = verifyVNParameter(entities, langCode, default)
	 		if verification then
		 		additionalCategory = verification
	 		end
	 	end
		if langCode == userLangCode then
			-- Already displayed in bold
		else
			local langName = _languageNamesByCode[langCode]
			additionalLang = false
			if not langName then
				langName = _additionalLanguageNamesByCode[langCode]
				additionalLang = true
			end

		 	vnEntry = getVNEntry(entities, langCode, langName, additionalLang, default, entities.useWikidata, false)
		 	if vnEntry then
			 	vn = vn .. vnEntry
			end
		end
	end

	-- Display error when wikidata values are incorrect
	additionalCategory = additionalCategory .. checkEntities(entities)

	-- Add Category:Biology_categories_without_double_wikidata_item
	if entities.entity and not entities.otherEntity then
		-- Commons category or gallery has a single wikidata item
		local name = suppressDisambiguation(mw.title.getCurrentTitle().text)
		local isSpecies = string.find(name, ' ', 1, true)
		if entities.entitySpecified then
			addDebug(nil,'getVN','entitySpecified + isSpecies=' .. tostring(isSpecies))
			if isSpecies then
				additionalCategory = additionalCategory .. '[[Category:Biology_pages_with_wikidata_item_specified_in_VN]]'
			else
				additionalCategory = additionalCategory .. '[[Category:Biology_pages_with_wikidata_item_specified_in_VN| ]]'
			end
		else
			if _common.isCurrentNamespaceACategory() then
				-- Commons category has a single wikidata item
				if isSpecies then
					-- Species Category without gallery may have only one wikidata item (the taxon)
				else
					additionalCategory = additionalCategory .. '[[Category:Biology_categories_without_double_wikidata_item]]'
				end
			end
		end
	elseif entities.entity and entities.otherEntity then
		-- Commons category has a double wikidata item
		additionalCategory = additionalCategory .. '[[Category:Biology_categories_with_double_wikidata_item]]'
	end

	-- Add small information about wikidata
	if string.len(vn) == 0 then
		vn = 'No common name has yet been provided in this ' .. _common.getCurrentNamespace()
		if not mw.wikibase then
			-- Case 1: wikidata library is not enabled
			vn = vn .. '. (Soon common names will be retrieved from wikidata)'
		elseif args.useWikidata and string.startsWith(args.useWikidata,'Q') then
			if entities.entity then
				-- Case 3
				vn = vn .. ' nor in ' .. getWikidataLinkFormat2(entities.entity)
			else
				-- Case 2
				vn = vn .. incorrectBiologyTemplateUsage('VN', 'Wikidata ' .. args.useWikidata .. ' is not accessible', 'VN')
			end
		elseif entities.useWikidata then
			-- Case 4.true, 5.true: args.useWikidata=nil + no problem
			-- Case 6.true:         args.useWikidata=1
			if entities.entity then
				vn = vn .. ' nor in ' .. getWikidataLinkFormat2(entities.entity)
				if entities.otherEntity then
					vn = vn .. ' nor in ' .. getWikidataLinkFormat2(entities.otherEntity)
				end
			else
				-- Impossible ?
				vn = vn .. ' and no [https://www.wikidata.org/w/index.php?button=&title=Special%3ASearch&limit=500&search=' .. string.gsub(mw.title.getCurrentTitle().text,' ','+') .. ' wikidata item] is associated with it'
			end
		else
			if entities.useWikidataIsCalculated then
				-- Case 4.false, 5.false: args.useWikidata=nil + (sciname!=<category or gallery name> or multiple VN in page) => don't display additional info in all VN
			else
				if entities.entity then
					-- Case 6.false
					vn = vn .. '. <small>(You could activate the search in ' .. getWikidataLinkFormat2(entities.entity) .. ' by adding useWikidata=1 inside {{VN}} )</small>'
				else
					if string.isNilOrEmpty(args.useWikidata) or string.isTrue(args.useWikidata) then
						-- Case 7.nil, 7.true
						vn = vn .. '. <small>(No [https://www.wikidata.org/w/index.php?button=&title=Special%3ASearch&limit=500&search=' .. string.gsub(mw.title.getCurrentTitle().text,' ','+') .. ' wikidata item] is associated with this ' .. _common.getCurrentNamespace() .. ')</small>'
					else
						-- Case 7.false => it is normal that VN is empty, associating with wikidata will change nothing
					end
				end
			end
		end
	else
		if not mw.wikibase then
			-- Case 1: wikidata library is not enabled
		elseif not entities.entity then
			if string.startsWith(args.useWikidata,'Q') or string.isNilOrEmpty(args.useWikidata) or string.isTrue(args.useWikidata) then
				-- Case 2, Case 7.nil, 7.true
				vn = vn .. ' <small>(Note: no [https://www.wikidata.org/w/index.php?button=&title=Special%3ASearch&search=' .. string.gsub(mw.title.getCurrentTitle().text,' ','+') .. ' wikidata item] is associated with this ' .. _common.getCurrentNamespace() .. ')</small>'
			else
				-- Case 7.false => it is normal that VN is empty, associating with wikidata will change nothing
			end			
		end
	end

	-- if entities.entity then
		-- Uncomment following line to dump all properties
		-- vn = vn .. '<BR/>' .. dumpPath('entities.entity', entities.entity)
		-- if entities.otherEntity then
			-- Uncomment following line when 'Access to arbitrary items' is allowed
			-- vn = vn .. '<BR/>' .. dumpPath('entities.otherEntity.claims', entities.otherEntity.claims)
		-- end
	-- end
	return vn .. additionalCategory .. calcAdditionalInterwiki(entities)
end

-- getVNTitle() is called by Template:VN to display '[modify wikidata]'
function getVNTitle(frame, args)
	-- Calc entity & otherEntity & useWikidata & useWikidataIsCalculated
	local entities = retrieveEntities(args)

	if not entities.useWikidata then
		return ''
	end
	if not entities.entity then
		return ''
	end

	local userLangCode = args.lang --frame:preprocess("{{int:Lang}}") or mw.language.getContentLanguage():getCode()
	--addDebug(nil,'getVNTitle','userLangCode=' .. tostring(userLangCode))
	local userLang = mw.language.new(userLangCode)
	--addDebug(nil, 'getVNTitle', 'userLang:Code=' .. userLang:getCode())
	local langEdit = userLang:lc(frame:preprocess("{{int:edit}}"))
	--addDebug(nil, 'getVNTitle', 'langEdit=' .. langEdit)

	local label = entities.entity:getLabel()
	if label then
		label = '\'' .. label .. '\''
	else
		label = entities.entity.id
	end

	local wikidataLabel = langEdit .. ' wikidata ' .. label
	local wikidataLabelOther = ''
	local wikidataImageLabel = 'Wikidata '   .. label .. ' linked to current ' .. _common.getCurrentNamespace()
	local wikidataImageLabelOther = ''
	if entities.entitySpecified then
		wikidataLabel = langEdit .. ' specified wikidata ' .. label
	end
	if entities.otherEntity then
		local otherLabel = entities.otherEntity:getLabel()
		if otherLabel then
			otherLabel = '\'' .. otherLabel .. '\''
		else
			otherLabel = entities.otherEntity.id
		end
		-- [modifier wikidata 'Category:Aa' linked to current category] [modifier wikidata 'Abacoleptus' main topic of 'Category:Aa']
		wikidataLabel      = wikidataLabel .. ' linked to current ' .. _common.getCurrentNamespace()
		wikidataLabelOther = langEdit .. ' wikidata ' .. otherLabel .. ' main topic of ' .. label
		wikidataImageLabelOther = 'Wikidata item '    .. otherLabel .. ' main topic (P301) of ' .. label
	else
		-- [modifier wikidata 'Abacoleptus']
	end

	local toReturn =           '&nbsp;<small>' .. mw.text.nowiki('[') .. '[[wikidata:' .. entities.entity.id      .. '|' .. wikidataLabel      .. ']]]</small>'
	toReturn     = toReturn .. ' [[Image:Wikidata-logo-without-paddings.svg|20px|' .. wikidataImageLabel      .. '|link=https://www.wikidata.org/wiki/' .. entities.entity.id      .. ']]'
	if entities.otherEntity then
		toReturn = toReturn .. '&nbsp;<small>' .. mw.text.nowiki('[') .. '[[wikidata:' .. entities.otherEntity.id .. '|' .. wikidataLabelOther .. ']]]</small>'
		toReturn = toReturn .. ' [[Image:Wikidata-logo-without-paddings.svg|20px|' .. wikidataImageLabelOther .. '|link=https://www.wikidata.org/wiki/' .. entities.otherEntity.id .. ']]'
	end

	return toReturn
end

local _acceptedParameters = {
	lang='lang',				-- provided by Template:VN
	sciname='sciname',			
	useWikidata='useWikidata',
	nocat='nocat',
	edit='edit' 				-- managed directly by Template:VN
}

function detectBadParameters(args)
	-- Fill _languageNamesByCode and _languageCodes
	calcLanguages('en')

	local toRet = ''
	for optionKey, optionValue in pairs(args) do
		if type(optionKey) == 'string' then
			-- normal argument pair: en=EnglishName, lang=fr
			if not _languageNamesByCode[optionKey] and not _additionalLanguageNamesByCode[optionKey] and not _acceptedParameters[optionKey] then
				if toRet ~= '' then
					toRet = toRet .. ', '
				end
				toRet = toRet .. '"' .. optionKey .. '"'
			end
		else
			-- incorrect argument pair: 1=strangeParameter
			if toRet ~= '' then
				toRet = toRet .. ', '
			end
			toRet = toRet .. '"' .. optionValue .. '"'
		end
	end
	if toRet == '' then
		return toRet
	else
		return _common.incorrectBiologyTemplateUsage('VN', 'Incorrect parameter(s) ' .. toRet, 'VN')
	end
end

-- returns "sciname" out od "''sciname'' author"
function extractSciNameOutOfDecoratedSciName(decoratedSciName)
	local startPos = string.find(decoratedSciName, "''", 1, true)
	if startPos then
		--addDebug(nil, 'extractSciNameOutOfDecoratedSciName', 'startPos= ' .. startPos)
		while string.sub(decoratedSciName, startPos, startPos) == "'" do
	    	startPos = startPos + 1
			--addDebug(nil, 'extractSciNameOutOfDecoratedSciName', 'incr startPos= ' .. startPos)
	    end
		local endPos = string.find(decoratedSciName, "''", startPos, true)
		if endPos then
			--addDebug(nil, 'extractSciNameOutOfDecoratedSciName', 'endPos= ' .. endPos)
			local sciName = string.sub(decoratedSciName, startPos, endPos-1)
			sciName = mw.text.trim(sciName)
			return sciName
		end
	end
	return nil
end

function calcSiteUrl(urlPrefix, siteIdStr)
	if urlPrefix then
		siteIdStr = mw.text.trim(siteIdStr)
		urlPrefix = mw.text.trim(urlPrefix)
		if urlPrefix == 'firstParamIsAnUrl' then
			return '[' .. siteIdStr .. ' this url]'
		else
			return '[' .. urlPrefix .. siteIdStr .. ' ' .. siteIdStr .. ']'
		end
	else
		return siteIdStr
	end
end

-- Compares the ITIS identifior provided to {{ITIS}} with the id stored in wikidata by property p815
-- If there is a difference, the returned string displays an error + adds category 'Pages with incorrect biology template usage'
-- See https://www.wikidata.org/wiki/Wikidata:Taxonomy_task_force for sitePropertyId
function compareSiteIdWithWikidata(frame, sitePropertyId, siteName, templateName, templateForms, urlPrefix)
	if not _common.isCurrentNamespaceACategoryOrAGallery() then
		-- We do checks only in galleries and categories (When you look at Template, no param is provided
		return ''
	end

	local commonsSiteId = string.trimOrNullify(frame.args['1'])
	if not commonsSiteId then
		return _common.incorrectBiologyTemplateUsage('Link', 'no ' .. siteName .. ' id has been provided.', templateName)
	end
	commonsSiteId = string.gsub(commonsSiteId, '&#61;', '=')

	local commonsSciName = string.trimOrNullify(frame.args['sciname'])
	if not commonsSciName then
		return _common.incorrectBiologyTemplateUsage('Link', 'no ' .. siteName .. ' sciName has been provided.', templateName)
	end
	local commonsDecoratedSciName = string.trimOrNullify(frame.args['decoratedSciname'])

	local validity = string.trimOrNullify(frame.args['validity'])
	if validity then
		validity = string.lower(validity)
		if validity == 'nv' then
			-- taxon is invalid => no check is possible
			return ''
		else
			-- validity parameter has an incorrect value
			return _common.incorrectBiologyTemplateUsage('Link', 'Incorrect parameter validity.', templateName)
		end
	end

	local checks = string.trimOrNullify(frame.args['checks'])
	if checks and string.lower(checks) == 'no' then
		-- checks have been disabled
		return ''
	end

	local entities = retrieveEntitiesSimple(nil)
	if not entities.entity then
		-- Wikidata library is not enabled => no check is possible
		-- Or there is no link to wikidata => no check is possible
		return ''
	end

	getScientificNamesFromWikidata(entities)
	if not tableIsEmpty(_scientificNamesFromWikidata) then
		if commonsSciName ~= 'none' then
			if not _scientificNamesFromWikidata[commonsSciName] then
				-- ITIS sciname and wikidata sciname are different => cannot compare ids
				addDebug(nil, 'compareSiteIdWithWikidata', 'Check Disabled: Different scientific name between www.wikidata.org/wiki/' .. entities.entity.id .. ' (' .. tableToString(_scientificNamesFromWikidata,false) .. ') and wikicommons (' .. commonsSciName .. ')')
				return ''
				--return ' <small>(Warning: different scientific name between [[wikidata:' .. entities.entity.id .. '|wikidata]] (' .. tableToString(_scientificNamesFromWikidata,false) .. ') and wikicommons (' .. commonsSciName .. '))</small>[[Category:Pages with biology property different than on Wikidata|' .. siteName .. ']]'
			end
		elseif commonsDecoratedSciName then
			local sciName = extractSciNameOutOfDecoratedSciName(commonsDecoratedSciName)
			if sciName then
				if not listContainsExactString(sciName, _scientificNamesFromWikidata) then
					-- wikidata sciname not included in ITIS sciname => cannot compare ids
					addDebug(nil, 'compareSiteIdWithWikidata', 'Check Disabled: Different exact scientific name between www.wikidata.org/wiki/' .. entities.entity.id .. ' (' .. tableToString(_scientificNamesFromWikidata,false) .. ') and wikicommons (' .. sciName .. ')')
					return ''
				else
					addDebug(nil, 'compareSiteIdWithWikidata', 'sciname (' .. sciName ..') extracted from decoratedScientificName (' .. commonsDecoratedSciName .. ') is part of wikidata scinames ' .. tableToString(_scientificNamesFromWikidata,false))
				end
			else
				if not stringContainsAnItemOfList(commonsDecoratedSciName, _scientificNamesFromWikidata) then
					-- wikidata sciname not included in ITIS decoratedSciname => cannot compare ids
					addDebug(nil, 'compareSiteIdWithWikidata', 'Check Disabled: Different scientific name between www.wikidata.org/wiki/' .. entities.entity.id .. ' (' .. tableToString(_scientificNamesFromWikidata,false) .. ') and wikicommons (' .. commonsDecoratedSciName .. ')')
					return ''
				else
					addDebug(nil, 'compareSiteIdWithWikidata', 'decoratedScientificName (' .. commonsDecoratedSciName .. ') is contained in wikidata scinames ' .. tableToString(_scientificNamesFromWikidata,false))
				end
			end
		end
	end

	if not isTemplateCalledOnlyOnce(templateName,templateForms,templateForms) then
		-- If template is called multiple times...
		addDebug(nil, 'compareSiteIdWithWikidata', 'Check Disabled: Template ' .. templateName .. ' is called multiple times')
		return ''
	end

	local problematicEntity = nil
	local wikidataSiteId = string.trimOrNullify(getProperty(entities.entity, true, {'claims', sitePropertyId, 1, 'mainsnak', 'datavalue', 'value'}))
	if wikidataSiteId then
		addDebug(nil, 'compareSiteIdWithWikidata', 'Property ' .. sitePropertyId .. ' found in entity: ' .. wikidataSiteId)
		problematicEntity = entities.entity
	else
		-- Property (like p815 for 'identifiant ITIS') is not defined in wikidata => no check possible
		if entities.otherEntity then
			wikidataSiteId = string.trimOrNullify(getProperty(entities.otherEntity, true, {'claims', sitePropertyId, 1, 'mainsnak', 'datavalue', 'value'}))
			if not wikidataSiteId then
				-- Property (like p815 for 'identifiant ITIS') is not defined in wikidata => no check possible
				addDebug(nil, 'compareSiteIdWithWikidata', 'Check Impossible: https://www.wikidata.org/wiki/' .. entities.entity.id ..  ' and https://www.wikidata.org/wiki/' .. entities.otherEntity.id .. ' do not contain the property ' .. sitePropertyId)
				return ''
			end
			addDebug(nil, 'compareSiteIdWithWikidata', 'Property ' .. sitePropertyId .. ' found in otherEntity: ' .. wikidataSiteId)
			problematicEntity = entities.otherEntity
		else
			addDebug(nil, 'compareSiteIdWithWikidata', 'Check Impossible: https://www.wikidata.org/wiki/' .. entities.entity.id .. ' does not contain the property ' .. sitePropertyId)
			return ''
		end
	end

	if wikidataSiteId == commonsSiteId then
		-- Both identifier are equal: perfect
		addDebug(nil, 'compareSiteIdWithWikidata', 'Everything Perfect: ' .. siteName .. ' id is ' .. commonsSiteId .. ' on wikicommons and wikidata')
		return ''
	end

	-- Both identifier are different
	local wikidataSiteId2 = string.trimOrNullify(getProperty(entities.entity, true, {'claims', sitePropertyId, 2, 'mainsnak', 'datavalue', 'value'}))
	if not wikidataSiteId2 and entities.otherEntity then
		wikidataSiteId2 = string.trimOrNullify(getProperty(entities.otherEntity, true, {'claims', sitePropertyId, 2, 'mainsnak', 'datavalue', 'value'}))
	end
	if not wikidataSiteId2 then
		-- Property (like p815 for 'identifiant ITIS') has only 1 value in wikidata
	elseif wikidataSiteId2 == commonsSiteId then
		-- Both identifier are equal: perfect
		addDebug(nil, 'compareSiteIdWithWikidata', 'Everything Perfect: ' .. siteName .. ' id is ' .. commonsSiteId .. ' on wikicommons and wikidata (second value)')
		return ''
	end
	
	addDebug(nil, 'compareSiteIdWithWikidata', 'urlPrefix=' .. tostring(urlPrefix))
	local wikidataSiteIdStr = wikidataSiteId
	if wikidataSiteId2 then
		wikidataSiteIdStr = wikidataSiteId .. ', ' .. wikidataSiteId2
	end
	return ' <small>(Warning: different ' .. siteName .. ' Id between ' .. getWikidataLinkFormat2(problematicEntity) .. ' (' .. calcSiteUrl(urlPrefix, wikidataSiteIdStr) .. ') and wikicommons (' .. calcSiteUrl(urlPrefix, commonsSiteId) .. '))</small>[[Category:Pages with biology property different than on Wikidata|' .. siteName .. ']]'
end

function getAdditionalCategoryAboutWikidata()
	if not _common.isCurrentNamespaceACategoryOrAGallery() then
		-- We do checks only in galleries and categories (When you look at Template, no param is provided
		return ''
	end

	if not mw.wikibase then
		-- Wikidata library is not enabled => no check is possible
		return ''
	end

	local wikicode = mw.title.getCurrentTitle():getContent()
	if wikicode then
		if string.find(wikicode,'redirect|',1,true) then
			-- Category contains a redirect => we are not interested
			return ''
		end
		if string.find(wikicode,'#REDIRECT',1,true) then
			-- Gallery contains a redirect => we are not interested
			return ''
		end
	else
		-- Called from preview before creation of page
	end

	local entity = mw.wikibase.getEntityObject()
	if entity then
		return '[[Category:Biology_pages_with_wikidata_link]]'
	else
		return '[[Category:Biology_pages_without_wikidata_link]]'
	end
end

-- This function is called by {{#invoke:Wikidata4Bio/sandbox|test}} to discover Lua ;-)
function test(frame,args)
	local entity = mw.wikibase.getEntity();

	local s = entity:getBestStatements( 'P1111111' )
	addDebug(nil, 'test0', 'getBestStatements(P1111111)=' .. mw.text.jsonEncode(s))
	
	s = entity:getBestStatements( property_P935 )
	addDebug(nil, 'test1', 'getBestStatements(P935)=' .. mw.text.jsonEncode(s))
	s = getProperty(entity, false, {'claims', 'P935'})
	addDebug(nil, 'test2', 'getProperty=' .. mw.text.jsonEncode(s))

	s = entity:getBestStatements( property_P1843 )
	addDebug(nil, 'test3', 'getBestStatements(P1843)=' .. mw.text.jsonEncode(s))
	for key, value in pairs(s) do
		addDebug(nil, 'test4', 'getBestStatements/pairs(P1843)=' .. mw.text.jsonEncode(value.mainsnak.datavalue.value))
	end

	local p225 = getProperty(entity, false, {'claims', 'P1843'})
	addDebug(nil, 'test5', 'getProperty(P1843)=' .. mw.text.jsonEncode(p225))
	if p225 then
		for key, value in pairs(p225) do
			name = getProperty(value, false, property_mainsnak_datavalue_value)
			addDebug(nil, 'test6', 'getProperty/getProperty(P1843)=' .. mw.text.jsonEncode(name))
		end
	end
	return getDebug()
end

----------------------------------------------------------------------------------------------------------------
---------- PUBLIC FUNCTIONS ------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------
local p = {}

local getArgs = require('Module:Arguments').getArgs

-- Simply add {{#invoke:Wikidata4Bio|dumpWikidata}} to a page
function p.dumpWikidata(frame)
	local entity = mw.wikibase.getEntity()
	if entity then
		return dumpPath('entity', entity)
	else
		return 'not associated with wikidata'
	end
end

function p.getVN(frame)
	-- getArgs() will concatenate frame.args (lang=) with Template:VN arguments (useWikidata=, explain=, sciname= and all the <lang>=)
	local args = getArgs(frame)
	-- addDebug(nil, 'p.getVN', tableToString(args,true))
	-- addDebug(nil, 'p.getVN', detectBadParameters(args))

	local vn = getVN(args)
	-- Now add debug traces if activated
	if _debug then
		vn = vn .. getDebug()
	end
	return vn .. getAdditionalCategoryAboutWikidata() .. detectBadParameters(args)
end

function p.getVNTitle(frame)
	local args = getArgs(frame)
	local title = getVNTitle(frame, args)
	if _debug then
		title = title .. getDebug()
	end
	return title
end

function p.getLanguagesManagedByVN(frame)
	local langs = getLanguagesManagedByVN(frame.args)
	return langs
end

-- Used by Template:Avibase (only species) with sciname
function p.compareAvibaseIdWithWikidata(frame)
	-- frame, sitePropertyId, siteName, templateName, templateForms
	return compareSiteIdWithWikidata(frame, 'P2026', 'Avibase', 'Avibase', {'{{Avibase'}, 'http://avibase.bsc-eoc.org/species.jsp?lang=EN&avibaseid=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:BioLib with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareBioLibIdWithWikidata(frame)
	-- frame, sitePropertyId, siteName, templateName, templateForms
	return compareSiteIdWithWikidata(frame, 'P838', 'BioLib', 'BioLib', {'{{BioLib'}, 'http://www.biolib.cz/en/taxon/id')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:EOL with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareEOLIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P830', 'EOL', 'EOL', {'{{EOL'}, 'http://www.eol.org/pages/')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:Faunaeur with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareFaunaeurIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P1895', 'Faunaeur', 'Faunaeur', {'{{Faunaeur'}, 'http://www.faunaeur.org/full_results.php?id=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:FishBase+species with sciname
function p.compareFishBaseSpeciesIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P938', 'FishBase', 'FishBase', {'{{FishBase'}, 'http://www.fishbase.org/Summary/speciesSummary.php?id=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

	-- Used by Template:Fungorum+species, Template:Fungorum+genus, Template:Fungorum+family and Template:Fungorum+taxon with sciname
function p.compareFungorumIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P1391', 'Fungorum', 'Fungorum', {'{{Fungorum'}, frame.args['urlPrefix'])
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:GBIF with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareGBIFIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P846', 'GBIF', 'GBIF', {'{{GBIF'}, 'http://www.gbif.org/species/')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:GRIN+family, Template:GRIN+subfamily, Template:tribe and Template:subtribe with sciname
function p.compareGRINURLWithWikidata(frame)
	local error = ''
	local rank = string.trimOrNullify(frame.args['rank'])
	if rank then
		local commonsSciName = string.trimOrNullify(frame.args['sciname'])
		if commonsSciName then
			if rank == 'family' then
				if not string.endsWith(commonsSciName,'ceae') then
					error = _common.incorrectBiologyTemplateUsage('GRIN family', 'GRIN family should be used on families only (ending with "ceae"). Please Use GRIN subfamily, GRIN tribe, GRIN subtribe, GRIN genus or GRIN species', 'GRIN family')
				end
			elseif rank == 'subfamily' then
				if not string.endsWith(commonsSciName,'oideae') then
					error = _common.incorrectBiologyTemplateUsage('GRIN subfamily', 'GRIN subfamily should be used on subfamilies only (ending with "oideae"). Please Use GRIN family, GRIN tribe, GRIN subtribe, GRIN genus or GRIN species', 'GRIN subfamily')
				end
			elseif rank == 'tribe' then
				if not string.endsWith(commonsSciName,'eae') then
					error = _common.incorrectBiologyTemplateUsage('GRIN tribe', 'GRIN tribe should be used on tribes only (ending with "eae"). Please Use GRIN family, GRIN subfamily, GRIN subtribe, GRIN genus or GRIN species', 'GRIN tribe')
				end
			elseif rank == 'subtribe' then
				if not string.endsWith(commonsSciName,'inae') then
					error = _common.incorrectBiologyTemplateUsage('GRIN subtribe', 'GRIN subtribe should be used on subtribes only (ending with "inae"). Please Use GRIN family, GRIN subfamily, GRIN tribe, GRIN genus or GRIN species', 'GRIN subtribe')
				end
			end
		-- else: Strange case treated by compareSiteIdWithWikidata()
		end
	end
	return compareSiteIdWithWikidata(frame, 'P1421', 'GRIN', 'GRIN', {'{{GRIN'}, 'firstParamIsAnUrl')
		.. error
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- NOT used by Template:IPNI, which should be renamed Template:IPNI+search because it does not use an id
function p.compareIPNIIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P961', 'IPNI', 'IPNI', {'{{IPNI'}, 'http://www.ipni.org/ipni/simplePlantNameSearch.do?find_wholeName=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:ITIS with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareITISIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P815', 'ITIS', 'ITIS', {'{{ITIS'}, 'http://www.itis.gov/servlet/SingleRpt/SingleRpt?search_topic=TSN&search_value=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:IUCN (only species) with sciname
function p.compareIUCNIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P627', 'IUCN', 'IUCN', {'{{IUCN'}, 'http://www.iucnredlist.org/apps/redlist/details/')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:MSW with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareMSWIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P959', 'MSW', 'MSW', {'{{MSW'}, 'https://www.departments.bucknell.edu/biology/resources/msw3/browse.asp?s=y&id=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:MycoBank with sciname
function p.compareMycoBankIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P962', 'MycoBank', 'MycoBank', {'{{MycoBank'}, 'http://www.mycobank.org/MycoTaxo.aspx?Link=T&Rec=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:NCBI with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareNCBIIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P685', 'NCBI', 'NCBI', {'{{NCBI'}, 'https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?lin=s&p=has_linkout&id=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:NRCS+Plants with sciname
function p.compareNRCSPlantsIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P1772', 'NRCS Plants', 'NRCS Plants', {'{{NRCS Plants'}, 'http://plants.usda.gov/core/profile?symbol=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:ThePlantList+species with sciname
function p.compareThePlantListIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P1070', 'ThePlantList', 'ThePlantList species', {'{{ThePlantList species'}, 'http://www.theplantlist.org/tpl1.1/record/')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:TPDB with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareTPDBIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P842', 'TPDB', 'TPDB', {'{{TPDB'}, 'http://fossilworks.org/bridge.pl?action=taxonInfo&is_real_user=1&taxon_no=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:Tropicos with sciname
function p.compareTropicosIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P960', 'Tropicos', 'Tropicos', {'{{Tropicos', '{{tropicos'}, 'http://www.tropicos.org/Name/')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end

-- Used by Template:WRMS and Template:WRMS+species with |sciname=none|decoratedSciname={{{2|{{PAGENAME}}}}}
function p.compareWoRMSIdWithWikidata(frame)
	return compareSiteIdWithWikidata(frame, 'P850', 'WoRMS', 'WRMS', {'{{WRMS'}, 'http://www.marinespecies.org/aphia.php?p=taxdetails&id=')
		.. getDebug() .. getAdditionalCategoryAboutWikidata()
end


function p.test(frame)
	return test(frame,frame.args)
end

function p.testcase_suppressDisambiguation(frame)
	-- for testcases
	return suppressDisambiguation(frame.args['1'])
end

function p.testcase_isLink(frame)
	-- for testcases (return string when normal function returns boolean)
	return tostring(not not isLink(frame.args['1']))
end

function p.testcase_calcVNEntry(frame)
	-- for testcases
	return mw.text.nowiki(calcVNEntry(frame.args['lang'], frame.args['interwiki'], frame.args['otherInterwiki'], frame.args['vnFromWikidata'], frame.args['vnSource'], frame.args['default'])) .. getDebug()
end

function p.testcase_getScientificNames()
	-- for testcases (return string when normal function returns table)
	local entities = retrieveEntitiesSimple(nil)
	return mw.text.nowiki(tableToString(getScientificNames(entities),false))
end

function p.testcase_extractSciNameOutOfDecoratedSciName(frame)
	-- for testcases (return 'nil' instead of nil)
	local sciname = tostring(extractSciNameOutOfDecoratedSciName(frame.args['1']))
	if _debug then
		sciname = sciname .. getDebug()
	end
	return sciname
end

return p