Module documentation

--script that retrieves basic data stored in Wikidata, for the datamodel, see https://www.mediawiki.org/wiki/Extension:Wikibase_Client/Lua

local p = {}

local linguistic = require('Module:Linguistic')
--local formatDate = require('Module:Complex date') only loaded when needed to save memory in large pages like Wikidata:List of properties/all
local fb = require('Module:Fallback')
local i18nmessages = mw.loadData('Module:i18n/wikidata')

-- Wiki-specific parameters
local defaultlang = mw.getCurrentFrame():preprocess("{{int:lang}}")
local defaultlink = 'wikidata'

local function i18n(str)
	local message = i18nmessages[str]
	if type(message) == 'string' then
		return message
	end
	return fb._langSwitch(message, defaultlang) .. ''
end

local function formatError( key, text )
	return error(i18n(key) .. (text or ''))
end

local function addTrackingCat(prop, cat)
	if not prop and not cat then
		return error("no property provided")
	end
	if not cat then
		cat = i18nmessages.trackingcat .. '/' .. string.upper(prop)
	end
	return '[[Category:' .. cat .. ']]'
end

local function removeBlanks(args)
	for i, j in pairs(args) do -- does not work ??
		if (j == '') or (j == '-') then args[i] = nil end
	end
	return args
end

local function formatTheUnknown() -- voir si on peut accorder/adapter l'usage de "inconnu"
	return i18n('somevalue')
end

local function isSpecial(snak)
	return snak.snaktype ~= 'value'
end

local function sameValue(snak, target)
	return not isSpecial(snak) and p.getRawvalue(snak) == target
end

local function showLang(statement, str) -- TODO (not yet in proper format)
	--adds a lang indication at the start of the string, based on data in statement
	local mainsnak = statement.mainsnak
	if isSpecial(mainsnak) then
		return str
	end

	local langlist = {}
	if mainsnak.datavalue.type == 'monolingualtext' then
		langlist = {mainsnak.datavalue.value.language}
	elseif statement.qualifiers and statement.qualifiers.P407 then
		local convertlangcode = mw.loadData('Module:Dictionary/lang codes')
		for i, j in pairs( statement.qualifiers.P407 ) do
			if not isSpecial(j) then
				local val = convertlangcode[j.datavalue.value['numeric-id']]
				table.insert(langlist, val)
			end
		end
	end
	if #langlist == 0 then
		return str
	else
		return '(' .. table.concat(langlist) .. ')' .. str
	end
end

function p.getEntity( val )
	if type(val) == 'table' then
		return val
	end
	return mw.wikibase.getEntityObject(val)
end

-- DATE FUNCTIONS
local function splitTimestamp(timestamp, calendar)
	local pattern = "(%W)(%d+)%-(%d+)%-(%d+)"
	local era, year, month, day = timestamp:match(pattern)

	if calendar == 'julian' then
	--todo  year, month, day = formatdate.gregorianToJulian( era .. year, month, day )
	end

	return {day = day, month = month, year = year, era = era, timestamp = timestamp, type = 'dateobject'}
end

local function rangeObject(begin, ending)
	local timestamp
	if begin then
		timestamp = begin.timestamp
	elseif ending then
		timestamp = ending.timestamp
	end
	return {begin = begin, ending = ending, timestamp = timestamp, type = 'rangeobject'}
end

local function dateObject(orig, params) -- transforme un snak en un nouvel objet utilisable par Module:Date complexe
	if not params then
		params = {}
	end

	local newobj = splitTimestamp(orig.time, orig.calendar) -- initalise l'object en mettant la valeur des dates

	newobj.precision = params.precision or orig.precision
	newobj.type = 'dateobject'
	return newobj
end

local function formatDatepoint(obj, params) -- TO IMPROVE
	if not obj then
		return nil
	end
	local formatDate = require('Module:Complex date')
	local lang = params.lang or defaultlang
	local precision = math.min(obj.precision, params.precision or 15) -- if we don't want to show the value to its full detail
	if precision >= 11 then
		return formatDate.complex_date{args={date1 = obj.year .. '-' .. obj.month .. '-' .. obj.day, lang= lang}}
	elseif precision == 10 then
		return formatDate.complex_date{args={date1 = obj.year .. '-' .. obj.month, lang= lang}}
	elseif precision == 9 then
		return formatDate.complex_date{args={date1 = tostring(obj.year), lang= lang}}
	elseif precision == 8 then
		return formatDate.complex_date{args={date1 = string.sub(tostring(obj.year), 1, 3) .. '0', lang = lang, precision = 'decade'}}
	elseif precision == 7 then
		return formatDate.complex_date{args={date1 = string.sub(tostring(obj.year + 100), 1, 2), lang = lang, precision = 'century'}}
	end
	return nil
end

local function formatDaterange(obj, params) --TODO
	local begin = formatDatepoint(obj.begin, params) or ''
	local ending = formatDatepoint(obj.ending, params) or ''
	return begin .. '-' .. ending
end

local function objectToText(obj, params)
	if obj.type == 'dateobject' then
		return formatDatepoint(obj, params)
	elseif obj.type == 'rangeobject' then
		return formatDaterange(obj, params)
	end
	return nil
end

local function tableToText(values, params) -- takes a list of already formatted values and make them a text
	if not values then
		return nil
	end
	return linguistic.conj(values, params.lang or defaultlang, params.conjtype)--linguistic.conj( values, params.lang, params.conjtype )
end

function p.getDate(obj)
--[[
returns an object containing a timestamp for easy sorting, and other data
	possible types of object:
		dateobject
			{timestamp = string, year = number, month = number, day = number, calendar = string}
		rangeobject
			{timestamp = string, begin = dateobject, ending = dateobject}
]]--
	if not obj then
		return nil
	end
	if type(obj) == 'string' then
		obj = p.getEntity(obj)
	end

	-- if obj is a statement with date, get it
	if obj.mainsnak and not isSpecial(obj.mainsnak) and obj.mainsnak.datatype == 'time' then
		return dateObject(obj.mainsnak.datavalue.value)
	end

	-- else preload relevant data
	local qualifs = obj.qualifiers -- when obj is a statement, look in qualifiers
	local claims = obj.claims -- when obj is an item, look in claims

	local pointprop = {'P585', 'P571'} -- dates corresponding to a punctual fact
	local beginprop = {'P580', 'P569'} -- start date, birth date == start of a date range
	local endingprop = {'P582', 'P570'}

	local function getval(prop)
		local val
		if claims and claims[prop] and not isSpecial(claims[prop][1].mainsnak) then
			val = claims[prop][1].mainsnak.datavalue.value
		elseif qualifs and qualifs[prop] and not isSpecial(qualifs[prop][1]) then
			val = qualifs[prop][1].datavalue.value
		end
		if val then
			return dateObject(val)
		end
		return nil
	end

	for i, prop in pairs(pointprop) do
		local val = getval(prop)
		if val then return val end
	end
	--if no date has not been found, look for startdate or enddate
	local begin, ending
	for i, prop in pairs(beginprop) do
		begin = getval(prop)
		if begin then
			break
		end
	end
	for i, prop in pairs(endingprop) do
		ending = getval(prop)
		if ending then
			break
		end
	end
	if begin or ending then
		return rangeObject(begin, ending)
	end
	return nil
end

function p.getFormattedDate(statement, params)
	local datetable = p.getDate(statement)
	if not datetable then
		return nil
	end
	return objectToText(datetable, params)
end

local function hasTargetValue(claim, target)
	if target == nil then
		return true
	end
	return sameValue(claim.mainsnak, target)
end

local function hasRank(claim, target)
	if target == 'valid' then
		return hasRank(claim, 'preferred') or hasRank(claim, 'normal')
	else
		return claim.rank == target
	end
end

local function bestRanked(claims)
	if not claims then
		return nil
	end
	local preferred, normal = {}, {}
	for _, j in ipairs(claims) do
		if j.rank == 'preferred' then
			table.insert(preferred, j)
		elseif j.rank == 'normal' then
			table.insert(normal, j)
		end
	end
	if #preferred > 0 then
		return preferred
	else
		return normal
	end
end

local function hasQualifier(claim, qualifier, qualifiervalues)
	if not qualifier then -- si aucun qualificatif est demandé, ça passe
		return true
	end

	qualifier = string.upper(qualifier)
	if not claim.qualifiers or not claim.qualifiers[qualifier] then
		return false
	end

	if type(qualifiervalues) == 'string' then
		qualifiervalues = mw.text.split(qualifiervalues, ',')
	end

	if (not qualifiervalues) or (qualifiervalues == {}) then
		return true -- si aucune valeur spécifique n'est exigée
	end

	for _, j in ipairs(claim.qualifiers[qualifier]) do
		for _, l in ipairs(qualifiervalues) do
			if p.sameValue(j, l) then
				return true
			end
		end
	end
	return false
 end

local function hasSource(statement, source, sourceproperty)
	if not statement.references then
		return false
	end
	sourceproperty = string.upper(sourceproperty or 'P248')
	local sourcevalue = string.upper(source or '')
	for _, ref in ipairs(statement.references) do
		for prop, content in pairs(ref.snaks) do
			if prop == sourceproperty then
				if sourcevalue == '' then
					return true
				else
					for _, k in ipairs(content) do
						if sameValue(k, source) then
							return true
						end
					end
				end
			end
		end
	end
	return false
end

local function hasDate(statement)
	if not statement.qualifiers then
		return false
	end
	local dateprops = {'P580', 'P585', 'P582'}
	for i, prop in pairs(dateprops) do
		if statement.qualifiers[prop] then
			return true
		end
	end
	return false
end

local function isInLanguage(snak, lang) -- ne fonctionne que pour les monolingualtext / étendre aux autres types en utilisant les qualifiers ?
	return not isSpecial(snak) and snak.datavalue.type == 'monolingualtext' and snak.datavalue.value.language == lang
end

local function numval(claims, numval) -- retourn les numval premières valeurs de la table claims
	local numval = tonumber(numval) or 0 -- raise an error if numval is not a positive integer ?
	if #claims <= numval then
		return claims
	end
	local newclaims = {}
	while #newclaims < numval do
		table.insert(newclaims, claims[#newclaims + 1])
	end
	return newclaims
end

local function wikipediaLink(entity, lang)
	local link

	local lg = string.gsub(lang, '-', '_')
	if (lg == 'be_tarask') then lg = 'be_x_old' end
	if (lg == 'nb') then lg = 'no' end

	if type(entity) == 'table' then
		link = entity:getSitelink(lg .. 'wiki')
	else
		link = mw.wikibase.getSitelink(entity, lg .. 'wiki')
	end
	if link then
		return ':' .. lang .. ':' .. link
	end
	return nil
end

local function getLink(entity, typelink, lang)
	if typelink == 'wikidata' then
		if type(entity) == 'table' then
			if entity.type == 'property' then
				return 'd:P:' .. entity.id
			elseif entity.type == 'lexeme' then
				return 'd:L:' .. entity.id
			else
				return 'd:' .. entity.id
			end
		else
			if string.sub(entity, 1, 1) == 'P' then
				return 'd:P:' .. entity
			elseif string.sub(entity, 1, 1) == 'L' then
				return 'd:L:' .. entity
			else
				return 'd:' .. entity
			end
		end

	elseif typelink == 'wikipedia' then
		return wikipediaLink(entity, lang or defaultlang)

	elseif typelink == 'anywikipedia' then
		for _, lg in ipairs(fb.fblist(lang or defaultlang, true)) do
			local link = wikipediaLink(entity, lg)
			
			if link then
				return link
			end
		end
		-- Last fallback language is ever 'en'. For entities which don’t have
		-- English Wikipedia pages, we should return any siteLink here, but
		-- `getSiteLinkList()` is not exposed in Lua API.
	end
	return nil
end

function p.comparedate(a, b) -- returns true if a is earlier than B or if a has a date but not b
	if a and b then
		return a.timestamp < b.timestamp
	elseif a then
		return true
	end
	return false
end

function p.chronosort(objs, inverted)
	table.sort(objs, function(a, b)
		local timeA = p.getDate(a)
		local timeB = p.getDate(b)
		if inverted then
			return p.comparedate(timeB, timeA)
		else
			return p.comparedate(timeA, timeB)
		end
	end)

	return objs
end

function p.sortclaims(claims, sorttype)
	if type(sorttype) == 'function' then
		table.sort(claims, sorttype)
	elseif sorttype == 'chronological' then
		return p.chronosort(claims)
	elseif sorttype == 'inverted' then
		return p.chronosort(claims, true)
	end
	return claims
end

function p.getRawvalue(snak)
	return p.getDatavalue(snak, { displayformat = 'raw' })
end

function p.showentity(entity, lang)
	if not entity then
		return nil
	end
	local label, link, id = p._getLabel(entity, lang), getLink(entity, 'wikidata')
	if type(entity) == 'table' then
		id = entity.id
	else
		id = entity
	end
	return '[[' .. link .. '|' .. label .. ']] <small>(' .. id .. ')</small>'
end

function p.getDatavalue(snak, params)
	if isSpecial(snak) then
		return nil
	end

	if not params then
		params = {}
	end

	local displayformat = params.displayformat
	local datatype = snak.datavalue.type
	local value = snak.datavalue.value

	if datatype == 'wikibase-entityid' then
		if type(displayformat) == 'function' then
			return displayformat(snak, params)
		end
		local id = snak.datavalue.value.id
		if displayformat == 'raw' then
			return id
		elseif displayformat == 'wikidatastyle' then
			return p.showentity(id, params.lang)
		else
			return p.formatEntity(id, params)
		end

	elseif datatype == 'string' then
		local showntext = params.showntext
		if displayformat == 'weblink' then
			if showntext then
				return '[' .. value .. ' ' .. showntext .. ']'
			else
				return value
			end
		end
		if snak.datatype == 'math' and displayformat ~= 'raw' then
			value = mw.getCurrentFrame():extensionTag('math', value)
		else
			if params.urlpattern then
				showntext = mw.text.nowiki(showntext or value)
				value = mw.ustring.gsub(value, '%%', '%%%%') -- escape '%'
				value = '[' .. mw.ustring.gsub(mw.ustring.gsub(params.urlpattern, '$1', value), ' ', '%%20') .. ' ' .. showntext .. ']'
			elseif params.pattern then
				local pattern = mw.ustring.gsub(params.pattern, '%%', '%%%%')
				value = mw.ustring.gsub(value, '%%', '%%%%')
				value = mw.ustring.gsub(pattern, '$1', value)
			else
				if displayformat ~= 'raw' then
					value = mw.text.nowiki(value)
				end
			end
		end
		return value

	elseif datatype == 'time' then -- format example: +00000001809-02-12T00:00:00Z
		if displayformat == 'raw' then
			return value.time
		else
			return objectToText(dateObject(value), params)
		end

	elseif datatype == 'globecoordinate' then
		-- retourne une table avec clés latitude, longitude, précision et globe à formater par un autre module (à changer ?)
		if displayformat == 'latitude' then
			return value.latitude
		elseif displayformat == 'longitude' then
			return value.longitude
		elseif displayformat == 'qualifier' then
			local coord = require 'Module:Coordinates'
			value.globe = mw.loadData('Module:Wikidata/Globes')[value.globe]
			value.precision = nil
			return coord._coord(value)
		else
			value.globe = mw.loadData('Module:Wikidata/Globes')[value.globe] -- transforme l'ID du globe en nom anglais utilisable par geohack
			return value -- note : les coordonnées Wikidata peuvent être utilisée depuis Module:Coordinates. Faut-il aussi autoriser à appeler Module:Coordiantes ici ?
		end

	elseif datatype == 'quantity' then -- todo : gérer les paramètre précision
		if displayformat == 'raw' then
			return tonumber(value.amount)
		else
			local formatNum = require 'Module:Formatnum'
			local number = formatNum.formatNum(value.amount, params.lang)
			local unit = mw.ustring.match(value.unit, '(Q%d+)')
			if unit then
				number = number .. '&nbsp;' .. p.formatEntity(unit, params)
			end
			return number
		end
	elseif datatype == 'monolingualtext' then
		return '<span lang="' .. value.language .. '">' .. value.text .. '</span>'
	else
		return formatError( 'unknown-datavalue-type', datatype )
	end
end

local function getMultipleClaims(args)
	local newargs = args
	local claims = {}
	for i, j in pairs(args.property) do
		newargs.property = j
		local newclaims = p.getClaims(args)
		if newclaims then
			for k, l in pairs(newclaims) do
				table.insert(claims, l)
			end
		end
	end
	return claims
end

function p.getClaims( args ) -- returns a table of the claims matching some conditions given in args
	args = removeBlanks(args)
	if not args.property then
		return formatError( 'property-param-not-provided' )
	end
	if type(args.property) == 'table' then
		return getMultipleClaims(args)
	end
	--Get entity
	if args.item then -- synonyms
		args.entity = args.item
	end
	local property = string.upper(args.property)
	local allClaims
	local entity = args.entity
	if type(entity) == 'table' then
		allClaims = (entity and entity.claims and entity.claims[property]) or {}
	else
		allClaims = mw.wikibase.getAllStatements(entity, property)
	end
	if #allClaims == 0 then
		return nil
	end

	if not args.rank then
		args.rank = 'best'
	end
	local claims = {}
	for _, statement in ipairs(allClaims) do
		if
			(
			not args.excludespecial
			or
			not (isSpecial(statement.mainsnak))
		)
		and
		(
			not args.targetvalue
			or
			hasTargetValue(statement, args.targetvalue)
		)
		and
		(
			not args.qualifier
			or
			hasQualifier(statement, args.qualifier, args.qualifiervalues or args.qualifiervalue)
		)
		and
		(
			not args.withsource or args.withsource == '-'
			or
			hasSource(statement, args.withsource, args.sourceproperty)
		)
		and
		(
			not args.isinlanguage
			or
			isInLanguage(statement.mainsnak, args.isinlanguage)
		)
		and
		(
			args.rank == 'best' -- rank == best est traité à a fin
			or
			hasRank(statement, args.rank)
		)
		then
			table.insert(claims, statement)
		end
	end
	if #claims == 0 then
		return nil
	end
	if args.rank == 'best' then
		claims = bestRanked(claims)
	end
	if args.sorttype then
		claims = p.sortclaims(claims, args.sorttype)
	end

	if args.numval then
		return numval(claims, args.numval)
	end
	return claims
end

function p.formatClaimList(claims, args)
	if not claims then
		return nil
	end
	for i, j in pairs(claims) do
		claims[i] = p.formatStatement(j, args)
	end
	return claims
end

function p.stringTable(args) -- like getClaims, but get a list of string rather than a list of snaks, for easier manipulation
	local claims = p.getClaims(args)
	return p.formatClaimList(claims, args)
end

local function getQualifiers(statement, qualifs, params)
	if not statement.qualifiers then
		return nil
	end
	local vals = {}
	for i, j in pairs(qualifs) do
		j = string.upper(j)
		if statement.qualifiers[j] then
			local inserted = false
			if statement.qualifiers[j][1].datatype == 'monolingualtext' then
				local in_preferred_lang
				for _, language in ipairs(fb.fblist(params.lang or defaultlang, true)) do
					for _, snak in ipairs(statement.qualifiers[j]) do
						if isInLanguage(snak, language) then
							in_preferred_lang = snak
							break
						end
					end
					if in_preferred_lang then
						break
					end
				end
				if in_preferred_lang then
					table.insert(vals, in_preferred_lang)
					inserted = true
				end
			end
			if not inserted then
				for _, snak in pairs(statement.qualifiers[j]) do
					table.insert(vals, snak)
				end
			end
		end
	end
	if #vals == 0 then
		return nil
	end
	return vals
end

function p.getFormattedQualifiers(statement, qualifs, params)
	if not params then params = {} end
	local qualiftable = getQualifiers(statement, qualifs, params)
	if not qualiftable then
		return nil
	end
	for i, j in pairs(qualiftable) do
		local params = params
		if j.datatype == 'globe-coordinate' then
			params.displayformat = 'qualifier'
		end
		qualiftable[i] = p.formatSnak(j, params)
	end
	return linguistic.conj(qualiftable, params.lang or defaultlang)
end

function p.formatStatement( statement, args )
	if not statement.type or statement.type ~= 'statement' then
		return formatError( 'unknown-claim-type', statement.type )
	end
	if not args then args = {} end
	local lang = args.lang or defaultlang
	local str = p.formatSnak( statement.mainsnak, args )
	if args.showlang == true then
		str = showLang(statement, str)
	end

	local qualifs = args.showqualifiers
	if qualifs then
		if type(qualifs) == 'string' then
			qualifs = mw.text.split(qualifs, ',')
		end
		local foundvalues = p.getFormattedQualifiers(statement, qualifs, args)
		if foundvalues then
			if args.delimiter then
				str = str .. args.delimiter .. foundvalues
			else
				str = str .. linguistic.inparentheses(foundvalues, lang)
			end
		end
	end

	if args.showdate then -- when "showdate and p.chronosort are both set, date retrieval is performed twice
		local timedata = p.getDate(statement)
		if timedata then
			local formatteddate = objectToText(timedata, args)
			formatteddate = linguistic.inparentheses(formatteddate, lang)
			str = str .. '<small>' .. formatteddate ..'</small>'
		end
	end

	if args.showsource and statement.references then
		local cite = require 'Module:Cite'
		local frame = mw.getCurrentFrame()
		local sourcestring = ''
		for _, ref in ipairs(statement.references) do
			if ref.snaks.P248 then
				for j, source in pairs(ref.snaks.P248) do
					if not isSpecial(source) then
						local page
						if ref.snaks.P304 and not isSpecial(ref.snaks.P304[1]) then
							page = ref.snaks.P304[1].datavalue.value
						end
						local s = cite.citeitem(source.datavalue.value.id, lang, page)
						s = frame:extensionTag( 'ref', s )
						sourcestring = sourcestring .. s
					end
				end
			elseif ref.snaks.P854 and not isSpecial(ref.snaks.P854[1]) then
				s = frame:extensionTag( 'ref', p.getDatavalue(ref.snaks.P854[1]) )
				sourcestring = sourcestring .. s
			end
		end
		str = str .. sourcestring
	end
	return str
end

function p.getmainid(claim)
	if claim and not isSpecial(claim.mainsnak) then
		return claim.mainsnak.datavalue.value.id
	end
	return nil
end

function p.formatSnak(snak, params)
	--local params = params or {} pour faciliter l'appel depuis d'autres modules
	if snak.snaktype == 'value' then
		return p.getDatavalue(snak, params)
	elseif snak.snaktype == 'somevalue' then
		return formatTheUnknown()
	elseif snak.snaktype == 'novalue' then
		return i18n('novalue') --todo
	else
		return formatError( 'unknown-snak-type', snak.snaktype )
	end
end

local function defaultLabel(entity, displayformat) -- label when no label is available
	if displayformat == 'id' then
		if type(entity) ~= 'table' then
			return entity
		else
			return entity.id
		end
	end
	return i18n('no-label')
end

function p._getLabel(entity, lang, default, fallback)
	if not entity then
		return nil
	end
	if not lang then
		lang = defaultlang
	end
	if type(entity) ~= 'table' and lang == defaultlang then
		local label, lg = mw.wikibase.getLabelWithLang(entity)
		if label and (fallback ~= '-' or lg == lang) then
			return label
		end
	else
		entity = p.getEntity(entity)
		if entity and entity.labels then
			if fallback ~= '-' then
				for _, lg in ipairs(fb.fblist(lang, true)) do
					if entity.labels[lg] then
						return entity.labels[lg].value
					end
				end
			else
				if entity.labels[lang] then
					return entity.labels[lang].value
				end
			end
		end
	end
	return defaultLabel(entity, default)
end

function p._getDescription(entity, lang, fallback)
	if not entity then
		return i18n('no description')
	end
	if not lang then
		lang = defaultlang
	end
	if type(entity) ~= 'table' and lang == defaultlang then
		local description, lg = mw.wikibase.getDescriptionWithLang(entity)
		if description and (fallback ~= '-' or lg == lang) then
			return description
		end
	else
		entity = p.getEntity(entity)
		if entity and entity.descriptions then
			if fallback ~= '-' then
				for _, lg in ipairs(fb.fblist(lang, true)) do
					if entity.descriptions[lg] then
						return entity.descriptions[lg].value
					end
				end
			else
				if entity.descriptions[lang] then
					return entity.descriptions[lang].value
				end
			end
		end
	end
	return i18n('no description')
end

local function formattedLabel(label, entity, args)
	local link = getLink(entity, args.link, args.lang)
	if not link then
		link = getLink(entity, defaultlink, args.lang)
	end
	if not link then
		return label
	else
		return '[[' .. link .. '|' .. label .. ']]'
	end
end

function p.formatEntity( entity, args )
	if not entity then
		return nil
	end
	if not args then
		args = {}
	end
	local label = p._getLabel(entity, args.lang, 'id', args.fallback)
	return formattedLabel(label, entity, args)
end

function p.getLabel(frame) -- simple for simple templates like {{Q|}}}
	local args = frame.args
	local entity = args.entity
	local lang = args.lang
	if not entity then
		return i18n('invalid-id')
	end

	if string.sub(entity, 1, 10) == 'Property:P' then
		entity = string.sub(entity, 10)
	elseif string.sub(entity, 1, 8) == 'Lexeme:L' then
		entity = string.sub(entity, 8)
	elseif not ({L = 1, P = 1, Q = 1})[string.sub(entity, 1, 1)] or not tonumber(string.sub(entity, 2)) then
		return i18n('invalid-id')
	end

	if not args.link or args.link == '' or args.link == '-' then -- by default: no link
		if lang == '' then
			lang = defaultlang
		end
		return p._getLabel(entity, lang, args.default, args.fallback)
	else
		return p.formatEntity(entity, args)
	end
end

function p._formatStatements( args )--Format statements and concat them cleanly
	if args.value == '-' then
		return nil
	end
	--If a value is already set, use it
	if args.value and args.value ~= '' then
		return args.value
	end
	local valuetable = p.stringTable(args)
	return tableToText(valuetable, args)
end

function p.showQualifier( args )
	local qualifs = args.qualifiers or args.qualifier
	if type(qualifs) == 'string' then
		qualifs = mw.text.split(qualifs, ',')
	end
	if not qualifs then
		return formatError( 'property-param-not-provided' )
	end
	local claims = p.getClaims(args)
	if not claims then
		return nil
	end
	local str = ''
	local new
	for _, cl in ipairs(claims) do
		new = p.getFormattedQualifiers(cl, qualifs, args) or ''
		str = str .. new
	end
	return str
end

function p._formatAndCat(args)
	local val = p._formatStatements(args)
	if val then
		return val .. addTrackingCat(args.property)
	end
	return nil
end

function p.getTheDate(args)
	local claims = p.getClaims(args)
	if not claims then
		return nil
	end
	local formattedvalues = {}
	for _, cl in ipairs(claims) do
		table.insert(formattedvalues, p.getFormattedDate(cl))
	end
	local val = linguistic.conj(formattedvalues)
	if val and args.addcat == true then
		return val .. addTrackingCat(args.property)
	else
		return val
	end
end

--[[
	Returns a Wikipedia page name (with interwiki prefix) which is equivalent page
	to given page in given language.
	
	Args:
	* sourceTitle: wiki page title which exists on sourceLang Wikipedia.
	* sourceLang: language code of Wikipedia where `sourceTitle` is an
	article. Defaults to `en`.
	* targetLang: language code of Wikipedia from which you want to get
	equivalent article to `sourceTitle`. Defaults to `defaultlang`.
	
	Bug: return value should not start with a colon because of T14974
]]
function p._getEquivalentWPArticle(sourceTitle, sourceLang, targetLang)
	if not sourceLang or sourceLang == '' then
		sourceLang = 'en'
	end
	
	if not targetLang or targetLang == '' then
		targetLang = defaultlang
	end
	
	local sourceLink = ' :' .. sourceLang .. ':' .. sourceTitle
	
	if sourceLang == targetLang then
		return sourceLink
	end
	
	local sourceWiki = sourceLang .. 'wiki' -- e.g. "enwiki" is global site id for English Wikipedia
	local id = mw.wikibase.getEntityIdForTitle( sourceTitle, sourceWiki )
	
	if not id then
		return sourceLink --source page has no linked Wikidata element, let’s keep it
	end
	
	local fallbackLangs = mw.language.getFallbacksFor(targetLang)
	table.insert(fallbackLangs, 1, targetLang) --keeps targetLang as first lang to try
	
	for _, fallbackLang in ipairs(fallbackLangs) do
		local existingEquivalent = getLink(id, 'anywikipedia', fallbackLang)
		if existingEquivalent then
			return ' ' .. existingEquivalent
		end
	end
	
	return sourceLink --source page has no interwiki, let’s keep it
end

function p.formatStatements( args )
	return p._formatStatements( args )
end

function p.getEntityFromId(id)
	return p.getEntity(id)
end

----------------------------------------
-- Functions callable from a template --
----------------------------------------

function p.getaDate(frame)
	return p.getTheDate(frame.args)
end

function p.getQualifier(frame)
	return p.showQualifier(frame.args)
end

function p.getDescription(frame) -- simple for simple templates like {{Q|}}}
	local entity = frame.args.entity
	if not entity then
		return i18n('invalid-id')
	end
	local lang = frame.args.lang
	local fallback = frame.args.fallback

	return p._getDescription(entity, lang, fallback)
end

function p.formatStatementsE(frame)
	local args = {}
	if frame == mw.getCurrentFrame() then
		args = frame:getParent().args -- paramètres du modèle appelant (est-ce vraiment une bonne idée ?)
		for k, v in pairs(frame.args) do
			args[k] = v
		end
	else
		args = frame
	end
	return p._formatStatements( args )
end

function p.formatAndCat(frame)
	local args = {}
	if frame == mw.getCurrentFrame() then
		args = frame:getParent().args -- paramètres du modèle appelant (est-ce vraiment une bonne idée ?)
		for k, v in pairs(frame.args) do
			args[k] = v
		end
	else
		args = frame
	end
	return p._formatAndCat( args )
end

function p.labelOf(frame)
	local id = frame.args[1]
	-- returns the label of the given entity/property id
	-- if no id is given, the one from the entity associated with the calling Wikipedia article is used
	if not id then
		local entity = mw.wikibase.getEntity()
		if not entity then return printError("entity-not-found") end
		id = entity.id
	end
	return mw.wikibase.label(id)
end

--[[
	Returns a wikilink (or only target) to the Wikipedia page with which the
	provided entity is linked.
	
	Args:
	* entity: Wikidata item id. e.g. Q220
	* lang: language code of Wikipedia which the link will target.
	* fallback: (optional) empty string or '-' to disable fallbacking to another 
	Wikipedia when no article is linked for Wikipedia in requested language.
	* label: (optional) link label. Defaults to entity label.
	* nolink: if set and different from empty string, only target page title is
	returned without any formatting.
]]
function p.getLink(frame)
	local entity = frame.args.entity
	if not entity then
		return i18n('invalid-id')
	end

	local fallback = frame.args.fallback
	local lang = frame.args.lang
	local label = frame.args.label
	local typelink = 'anywikipedia'

	if (fallback == '') or (fallback == '-') then typelink = 'wikipedia' end

	if (not label) or (label == '') or (label == '-') then
		label = p._getLabel(entity, lang, 'id', fallback)
	end

	-- If no Wikipedia page could be found as fallback, we use the label as a
	-- non-existing page title (red link style).
	local target = getLink(entity, typelink, lang) or lang .. ':' .. label

	local noLink = frame.args.nolink
	if (not noLink) or (noLink == '') then
		return '[[' .. target .. '|' .. label .. ']]'
	else
		return '&#32;' .. target -- &#32; workarrounds T14974 issue
	end
end

--[[
	Returns a Wikipedia page name with interwiki prefix which is equivalent page
	to given page in given language.
	
	Args:
	* sourceTitle: wiki page title which exists on `sourceLang` Wikipedia.
	* sourceLang: language code of Wikipedia where `sourceTitle` is an
	article. Defaults to `en`.
	* targetLang: language code of Wikipedia from which you want to get
	equivalent article to `sourceTitle`. Defaults to `defaultlang`.
]]
function p.getEquivalentWPArticle(frame)
	return p._getEquivalentWPArticle(
		frame.args.sourceTitle,
		frame.args.sourceLang,
		frame.args.targetLang
	)
end

return p