มอดูล:Wd/sandbox

-- Original module located at :en:Module:Wd and :en:Module:Wd/i18n.
require("strict")
local p = {}
local arg = ...
local i18n
local function loadI18n(aliasesP, frame)
	local title
	if frame then
		-- current module invoked by page/template, get its title from frame
		title = frame:getTitle()
	else
		-- current module included by other module, get its title from ...
		title = arg
	end
	if not i18n then
		i18n = require(title .. "/i18n").init(aliasesP)
	end
end
p.claimCommands = {
	property   = "property",
	properties = "properties",
	qualifier  = "qualifier",
	qualifiers = "qualifiers",
	reference  = "reference",
	references = "references"
}
p.generalCommands = {
	label       = "label",
	title       = "title",
	description = "description",
	alias       = "alias",
	aliases     = "aliases",
	badge       = "badge",
	badges      = "badges"
}
p.flags = {
	linked        = "linked",
	short         = "short",
	raw           = "raw",
	multilanguage = "multilanguage",
	unit          = "unit",
	-------------
	preferred     = "preferred",
	normal        = "normal",
	deprecated    = "deprecated",
	best          = "best",
	future        = "future",
	current       = "current",
	former        = "former",
	edit          = "edit",
	editAtEnd     = "edit@end",
	mdy           = "mdy",
	single        = "single",
	sourced       = "sourced"
}
p.args = {
	eid  = "eid",
	page = "page",
	date = "date"
}
local aliasesP = {
	coord                   = "P625",
	-----------------------
	image                   = "P18",
	author                  = "P50",
	authorNameString        = "P2093",
	publisher               = "P123",
	importedFrom            = "P143",
	wikimediaImportURL      = "P4656",
	statedIn                = "P248",
	pages                   = "P304",
	language                = "P407",
	hasPart                 = "P527",
	publicationDate         = "P577",
	startTime               = "P580",
	endTime                 = "P582",
	chapter                 = "P792",
	retrieved               = "P813",
	referenceURL            = "P854",
	sectionVerseOrParagraph = "P958",
	archiveURL              = "P1065",
	title                   = "P1476",
	formatterURL            = "P1630",
	quote                   = "P1683",
	shortName               = "P1813",
	definingFormula         = "P2534",
	archiveDate             = "P2960",
	inferredFrom            = "P3452",
	typeOfReference         = "P3865",
	column                  = "P3903",
	subjectNamedAs          = "P1810",
	wikidataProperty        = "P1687",
	publishedIn             = "P1433"
}
local aliasesQ = {
	percentage              = "Q11229",
	prolepticJulianCalendar = "Q1985786",
	citeWeb                 = "Q5637226",
	citeQ                   = "Q22321052"
}
local parameters = {
	property  = "%p",
	qualifier = "%q",
	reference = "%r",
	alias     = "%a",
	badge     = "%b",
	separator = "%s",
	general   = "%x"
}
local formats = {
	property              = "%p%s%r",
	qualifier             = "%q%s%r",
	reference             = "%r",
	propertyWithQualifier = "%p (%q)%s%r",
	alias                 = "%a%s",
	badge                 = "%b%s"
}
local hookNames = {              -- {level_1, level_2}
	parameters.property         = {"getProperty"},
	parameters.reference        = {"getReferences", "getReference"},
	parameters.qualifier        = {"getAllQualifiers"},
	parameters.qualifier.."\\d" = {"getQualifiers", "getQualifier"},
	parameters.alias            = {"getAlias"},
	parameters.badge            = {"getBadge"}
}
-- default value objects, should NOT be mutated but instead copied
local defaultSeparators = {
	"sep"      = {" "},
	"sep%s"    = {","},
	"sep%q"    = {"; "},
	"sep%q\\d" = {", "},
	"sep%r"    = nil,  -- none
	"punc"     = nil   -- none
}
local rankTable = {
	"preferred"  = 1,
	"normal"     = 2,
	"deprecated" = 3
}
local function replaceAlias(id)
	if aliasesPid then
		id = aliasesPid
	end
	return id
end
local function errorText(code, param)
	local text = i18n"errors"code
	if param then text = mw.ustring.gsub(text, "$1", param) end
	return text
end
local function throwError(errorMessage, param)
	error(errorText(errorMessage, param))
end
local function replaceDecimalMark(num)
	return mw.ustring.gsub(num, ".", i18n'numericdecimal-mark', 1)
end
local function padZeros(num, numDigits)
	local numZeros
	local negative = false
	if num  bY then
		return false
	end
	if aM  bM then
		return false
	end
	if aD  2 then
		return hookNamesparam:sub(1, 2).."\\d"index
	else
		return nil
	end
end
local function alwaysTrue()
	return true
end
-- The following function parses a format string.
--
-- The example below shows how a parsed string is structured in memory.
-- Variables other than 'str' and 'child' are left out for clarity's sake.
--
-- Example:
-- "A %p B %s%q1 C %r D"
--
-- Structure:
-- 
--   {
--     str = "A "
--   },
--   {
--     str = "%p"
--   },
--   {
--     str = " B ",
--     child =
--     
--       {
--         str = "%s",
--         child =
--         
--           {
--             str = "%q1"
--           }
--         
--       }
--     
--   },
--   {
--     str = " C ",
--     child =
--     
--       {
--         str = "%r"
--       }
--     
--   },
--   {
--     str = " D"
--   }
-- 
--
local function parseFormat(str)
	local chr, esc, param, root, cur, prev, new
	local params = {}
	local function newObject(array)
		local obj = {}  -- new object
		obj.str = ""
		array#array + 1 = obj  -- array{object}
		obj.parent = array
		return obj
	end
	local function endParam()
		if param > 0 then
			if cur.str ~= "" then
				cur.str = "%"..cur.str
				cur.param = true
				paramscur.str = true
				cur.parent.reqcur.str = true
				prev = cur
				cur = newObject(cur.parent)
			end
			param = 0
		end
	end
	root = {}  -- array
	root.req = {}
	cur = newObject(root)
	prev = nil
	esc = false
	param = 0
	for i = 1, #str do
		chr = str:sub(i,i)
		if not esc then
			if chr == '\\' then
				endParam()
				esc = true
			elseif chr == '%' then
				endParam()
				if cur.str ~= "" then
					cur = newObject(cur.parent)
				end
				param = 2
			elseif chr ==  then
				endParam()
				if prev and cur.str == "" then
					table.remove(cur.parent)
					cur = prev
				end
				cur.child = {}  -- new array
				cur.child.req = {}
				cur.child.parent = cur
				cur = newObject(cur.child)
			elseif chr ==  then
				endParam()
				if cur.parent.parent then
					new = newObject(cur.parent.parent.parent)
					if cur.str == "" then
						table.remove(cur.parent)
					end
					cur = new
				end
			else
				if param > 1 then
					param = param - 1
				elseif param == 1 then
					if not chr:match('%d') then
						endParam()
					end
				end
				cur.str = cur.str .. replaceSpecialChar(chr)
			end
		else
			cur.str = cur.str .. chr
			esc = false
		end
		prev = nil
	end
	endParam()
	-- make sure that at least one required parameter has been defined
	if not next(root.req) then
		throwError("missing-required-parameter")
	end
	-- make sure that the separator parameter "%s" is not amongst the required parameters
	if root.reqparameters.separator then
		throwError("extra-required-parameter", parameters.separator)
	end
	return root, params
end
local function sortOnRank(claims)
	local rankPos
	local ranks =   -- preferred, normal, deprecated, (default)
	local sorted = {}
	for _, v in ipairs(claims) do
		rankPos = rankTablev.rank or 4
		ranksrankPos#ranksrankPos + 1 = v
	end
	sorted = ranks1
	sorted = mergeArrays(sorted, ranks2)
	sorted = mergeArrays(sorted, ranks3)
	return sorted
end
local Config = {}
-- allows for recursive calls
function Config:new()
	local cfg = {}
	setmetatable(cfg, self)
	self.__index = self
	cfg.separators = {
		-- single value objects wrapped in arrays so that we can pass by reference
		"sep"   = {copyTable(defaultSeparators"sep")},
		"sep%s" = {copyTable(defaultSeparators"sep%s")},
		"sep%q" = {copyTable(defaultSeparators"sep%q")},
		"sep%r" = {copyTable(defaultSeparators"sep%r")},
		"punc"  = {copyTable(defaultSeparators"punc")}
	}
	cfg.entity = nil
	cfg.entityID = nil
	cfg.propertyID = nil
	cfg.propertyValue = nil
	cfg.qualifierIDs = {}
	cfg.qualifierIDsAndValues = {}
	cfg.bestRank = true
	cfg.ranks = {true, true, false}  -- preferred = true, normal = true, deprecated = false
	cfg.foundRank = #cfg.ranks
	cfg.flagBest = false
	cfg.flagRank = false
	cfg.periods = {true, true, true}  -- future = true, current = true, former = true
	cfg.flagPeriod = false
	cfg.atDate = {parseDate(os.date('!%Y-%m-%d'))}  -- today as {year, month, day}
	cfg.mdyDate = false
	cfg.singleClaim = false
	cfg.sourcedOnly = false
	cfg.editable = false
	cfg.editAtEnd = false
	cfg.inSitelinks = false
	cfg.langCode = mw.language.getContentLanguage().code
	cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode)
	cfg.langObj = mw.language.new(cfg.langCode)
	cfg.siteID = mw.wikibase.getGlobalSiteId()
	cfg.states = {}
	cfg.states.qualifiersCount = 0
	cfg.curState = nil
	cfg.prefetchedRefs = nil
	return cfg
end
local State = {}
function State:new(cfg, type)
	local stt = {}
	setmetatable(stt, self)
	self.__index = self
	stt.conf = cfg
	stt.type = type
	stt.results = {}
	stt.parsedFormat = {}
	stt.separator = {}
	stt.movSeparator = {}
	stt.puncMark = {}
	stt.linked = false
	stt.rawValue = false
	stt.shortName = false
	stt.anyLanguage = false
	stt.unitOnly = false
	stt.singleValue = false
	return stt
end
-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short)
	local label = nil
	local prefix, title= "", nil
	if not id then
		id = mw.wikibase.getEntityIdForCurrentPage()
		if not id then
			return ""
		end
	end
	id = id:upper()  -- just to be sure
	if raw then
		-- check if given id actually exists
		if mw.wikibase.isValidEntityId(id) and mw.wikibase.entityExists(id) then
			label = id
		end
		prefix, title = "d:Special:EntityPage/", label -- may be nil
	else
		-- try short name first if requested
		if short then
			label = p._property{aliasesP.shortName, p.args.eid = id}  -- get short name
			if label == "" then
				label = nil
			end
		end
		-- get label
		if not label then
			label = mw.wikibase.getLabelByLang(id, self.langCode) -- XXX: should use fallback labels?
		end
	end
	if not label then
		label = ""
	elseif link then
		-- build a link if requested
		if not title then
			if id:sub(1,1) == "Q" then
				title = mw.wikibase.getSitelink(id)
			elseif id:sub(1,1) == "P" then
				-- properties have no sitelink, link to Wikidata instead
				prefix, title = "d:Special:EntityPage/", id
			end
		end
		label = mw.text.nowiki(label) -- escape raw label text so it cannot be wikitext markup
		if title then
			label = buildWikilink(prefix .. title, label)
		end
	end
	return label
end
function Config:getEditIcon()
	local value = ""
	local prefix = ""
	local front = " "
	local back = ""
	if self.entityID:sub(1,1) == "P" then
		prefix = "Property:"
	end
	if self.editAtEnd then
		front = 
		back = 
	end
	value = "File:OOjs UI icon edit-ltr-progressive.svg|frameless|text-top|10px|alt=" .. i18n'infoedit-on-wikidata' .. "|link=https://www.wikidata.org/wiki/" .. prefix .. self.entityID .. "?uselang=" .. self.langCode
	if self.propertyID then
		value = value .. "#" .. self.propertyID
	elseif self.inSitelinks then
		value = value .. "#sitelinks-wikipedia"
	end
	value = value .. "|" .. i18n'infoedit-on-wikidata' .. ""
	return front .. value .. back
end
-- used to create the final output string when it's all done, so that for references the
-- function extensionTag("ref", ...) is only called when they really ended up in the final output
function Config:concatValues(valuesArray)
	local outString = ""
	local j, skip
	for i = 1, #valuesArray do
		-- check if this is a reference
		if valuesArrayi.refHash then
			j = i - 1
			skip = false
			-- skip this reference if it is part of a continuous row of references that already contains the exact same reference
			while valuesArrayj and valuesArrayj.refHash do
				if valuesArrayi.refHash == valuesArrayj.refHash then
					skip = true
					break
				end
				j = j - 1
			end
			if not skip then
				-- add  tag with the reference's hash as its name (to deduplicate references)
				outString = outString .. mw.getCurrentFrame():extensionTag("ref", valuesArrayi1, {name = valuesArrayi.refHash})
			end
		else
			outString = outString .. valuesArrayi1
		end
	end
	return outString
end
function Config:convertUnit(unit, raw, link, short, unitOnly)
	local space = " "
	local label = ""
	local itemID
	if unit == "" or unit == "1" then
		return nil
	end
	if unitOnly then
		space = ""
	end
	itemID = parseWikidataURL(unit)
	if itemID then
		if itemID == aliasesQ.percentage then
			return "%"
		else
			label = self:getLabel(itemID, raw, link, short)
			if label ~= "" then
				return space .. label
			end
		end
	end
	return ""
end
function State:getValue(snak)
	return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.unitOnly, false, self.type:sub(1,2))
end
function Config:getValue(snak, raw, link, short, anyLang, unitOnly, noSpecial, type)
	if snak.snaktype == 'value' then
		local datatype = snak.datavalue.type
		local subtype = snak.datatype
		local datavalue = snak.datavalue.value
		if datatype == 'string' then
			if subtype == 'url' and link then
				-- create link explicitly
				if raw then
					-- will render as a linked number like 1
					return "" .. datavalue .. ""
				else
					return "" .. datavalue .. " " .. datavalue .. ""
				end
			elseif subtype == 'commonsMedia' then
				if link then
					return buildWikilink("c:File:" .. datavalue, datavalue)
				elseif not raw then
					return "File:" .. datavalue .. ""
				else
					return datavalue
				end
			elseif subtype == 'geo-shape' and link then
				return buildWikilink("c:" .. datavalue, datavalue)
			elseif subtype == 'math' and not raw then
				local attribute = nil
				if (type == parameters.property or (type == parameters.qualifier and self.propertyID == aliasesP.hasPart)) and snak.property == aliasesP.definingFormula then
					attribute = {qid = self.entityID}
				end
				return mw.getCurrentFrame():extensionTag("math", datavalue, attribute)
			elseif subtype == 'external-id' and link then
				local url = p._property{aliasesP.formatterURL, p.args.eid = snak.property}  -- get formatter URL
				if url ~= "" then
					url = mw.ustring.gsub(url, "$1", datavalue)
					return "" .. url .. " " .. datavalue .. ""
				else
					return datavalue
				end
			else
				return datavalue
			end
		elseif datatype == 'monolingualtext' then
			if anyLang or datavalue'language' == self.langCode then
				return datavalue'text'
			else
				return nil
			end
		elseif datatype == 'quantity' then
			local value = ""
			local unit
			if not unitOnly then
				-- get value and strip + signs from front
				value = mw.ustring.gsub(datavalue'amount', "^%+(.+)$", "%1")
				if raw then
					return value
				end
				-- replace decimal mark based on locale
				value = replaceDecimalMark(value)
				-- add delimiters for readability
				value = i18n.addDelimiters(value)
			end
			unit = self:convertUnit(datavalue'unit', raw, link, short, unitOnly)
			if unit then
				value = value .. unit
			end
			return value
		elseif datatype == 'time' then
			local y, m, d, p, yDiv, yRound, yFull, value, calendarID, dateStr
			local yFactor = 1
			local sign = 1
			local prefix = ""
			local suffix = ""
			local mayAddCalendar = false
			local calendar = ""
			local precision = datavalue'precision'
			if precision == 11 then
				p = "d"
			elseif precision == 10 then
				p = "m"
			else
				p = "y"
				yFactor = 10^(9-precision)
			end
			y, m, d = parseDate(datavalue'time', p)
			if y = 6 then
					mayAddCalendar = true
					if precision = 1 and y ~= 0 then
						yFull = yRound * yFactor
						yReFactor = 1e9
						yReDiv = yFull / yReFactor
						yReRound = math.floor(yReDiv + 0.5)
						if yReDiv == yReRound then
							-- change precision to billions of years if we're in that range
							precision = 0
							yFactor = yReFactor
							yRound = yReRound
						else
							yReFactor = 1e6
							yReDiv = yFull / yReFactor
							yReRound = math.floor(yReDiv + 0.5)
							if yReDiv == yReRound then
								-- change precision to millions of years if we're in that range
								precision = 3
								yFactor = yReFactor
								yRound = yReRound
							end
						end
					end
					if not raw then
						if precision == 3 then
							suffix = i18n'datetimesuffixesmillion-years'
						elseif precision == 0 then
							suffix = i18n'datetimesuffixesbillion-years'
						else
							yRound = yRound * yFactor
							if yRound == 1 then
								suffix = i18n'datetimesuffixesyear'
							else
								suffix = i18n'datetimesuffixesyears'
							end
						end
					else
						yRound = yRound * yFactor
					end
				end
			else
				yRound = y
				mayAddCalendar = true
			end
			if mayAddCalendar then
				calendarID = parseWikidataURL(datavalue'calendarmodel')
				if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then
					if not raw then
						if link then
							calendar = " ("..buildWikilink(i18n'datetimejulian-calendar', i18n'datetimejulian')..")"
						else
							calendar = " ("..i18n'datetimejulian'..")"
						end
					else
						calendar = "/"..i18n'datetimejulian'
					end
				end
			end
			if not raw then
				local ce = nil
				if sign = 1 - (1 / 60) and precision = (1 / 60) - (1 / 3600) and precision = 1 then
				unitsPerDegree = 1
			elseif precision >= (1 / 60)  then
				unitsPerDegree = 60
			else
				unitsPerDegree = 3600
			end
			numDigits = math.ceil(-math.log10(unitsPerDegree * precision))
			if numDigits = 1 then
				latDegrees = strFormat:format(latitude)
				lonDegrees = strFormat:format(longitude)
				if not raw then
					latDegSym = replaceDecimalMark(latDegrees) .. degSymbol
					lonDegSym = replaceDecimalMark(lonDegrees) .. degSymbol
				else
					latDegSym = latDegrees .. degSymbol
					lonDegSym = lonDegrees .. degSymbol
				end
			else
				latConv = math.floor(latitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits
				lonConv = math.floor(longitude * unitsPerDegree * 10^numDigits + 0.5) / 10^numDigits
				if precision >= (1 / 60) then
					latMinutes = latConv
					lonMinutes = lonConv
				else
					latSeconds = latConv
					lonSeconds = lonConv
					latMinutes = math.floor(latSeconds / 60)
					lonMinutes = math.floor(lonSeconds / 60)
					latSeconds = strFormat:format(latSeconds - (latMinutes * 60))
					lonSeconds = strFormat:format(lonSeconds - (lonMinutes * 60))
					if not raw then
						latSecSym = replaceDecimalMark(latSeconds) .. secSymbol
						lonSecSym = replaceDecimalMark(lonSeconds) .. secSymbol
					else
						latSecSym = latSeconds .. secSymbol
						lonSecSym = lonSeconds .. secSymbol
					end
				end
				latDegrees = math.floor(latMinutes / 60)
				lonDegrees = math.floor(lonMinutes / 60)
				latDegSym = latDegrees .. degSymbol
				lonDegSym = lonDegrees .. degSymbol
				latMinutes = latMinutes - (latDegrees * 60)
				lonMinutes = lonMinutes - (lonDegrees * 60)
				if precision >= (1 / 60) then
					latMinutes = strFormat:format(latMinutes)
					lonMinutes = strFormat:format(lonMinutes)
					if not raw then
						latMinSym = replaceDecimalMark(latMinutes) .. minSymbol
						lonMinSym = replaceDecimalMark(lonMinutes) .. minSymbol
					else
						latMinSym = latMinutes .. minSymbol
						lonMinSym = lonMinutes .. minSymbol
					end
				else
					latMinSym = latMinutes .. minSymbol
					lonMinSym = lonMinutes .. minSymbol
				end
			end
			latValue = latDegSym .. latMinSym .. latSecSym .. latDirection
			lonValue = lonDegSym .. lonMinSym .. lonSecSym .. lonDirection
			value = latValue .. separator .. lonValue
			if link then
				globe = parseWikidataURL(datavalue'globe')
				if globe then
					globe = mw.wikibase.getLabelByLang(globe, "en"):lower()
				else
					globe = "earth"
				end
				latLink = table.concat({latDegrees, latMinutes, latSeconds}, "_")
				lonLink = table.concat({lonDegrees, lonMinutes, lonSeconds}, "_")
				value = " "..value..""
			end
			return value
		elseif datatype == 'wikibase-entityid' then
			local label
			local itemID = datavalue'numeric-id'
			if subtype == 'wikibase-item' then
				itemID = "Q" .. itemID
			elseif subtype == 'wikibase-property' then
				itemID = "P" .. itemID
			else
				return  .. errorText('unknown-data-type', subtype) .. 
			end
			label = self:getLabel(itemID, raw, link, short)
			if label == "" then
				label = nil
			end
			return label
		else
			return  .. errorText('unknown-data-type', datatype) .. 
		end
	elseif snak.snaktype == 'somevalue' and not noSpecial then
		if raw then
			return " "  -- single space represents 'somevalue'
		else
			return i18n'valuesunknown'
		end
	elseif snak.snaktype == 'novalue' and not noSpecial then
		if raw then
			return ""  -- empty string represents 'novalue'
		else
			return i18n'valuesnone'
		end
	else
		return nil
	end
end
function Config:getSingleRawQualifier(claim, qualifierID)
	local qualifiers
	if claim.qualifiers then qualifiers = claim.qualifiersqualifierID end
	if qualifiers and qualifiers1 then
		return self:getValue(qualifiers1, true)  -- raw = true
	else
		return nil
	end
end
function Config:snakEqualsValue(snak, value)
	local snakValue = self:getValue(snak, true)  -- raw = true
	if snakValue and snak.snaktype == 'value' and snak.datavalue.type == 'wikibase-entityid' then value = value:upper() end
	return snakValue == value
end
function Config:setRank(rank)
	local rankPos
	if rank == p.flags.best then
		self.bestRank = true
		self.flagBest = true  -- mark that 'best' flag was given
		return
	end
	if rank:sub(1,9) == p.flags.preferred then
		rankPos = 1
	elseif rank:sub(1,6) == p.flags.normal then
		rankPos = 2
	elseif rank:sub(1,10) == p.flags.deprecated then
		rankPos = 3
	else
		return
	end
	-- one of the rank flags was given, check if another one was given before
	if not self.flagRank then
		self.ranks = {false, false, false}  -- no other rank flag given before, so unset ranks
		self.bestRank = self.flagBest       -- unsets bestRank only if 'best' flag was not given before
		self.flagRank = true                -- mark that a rank flag was given
	end
	if rank:sub(-1) == "+" then
		for i = rankPos, 1, -1 do
			self.ranksi = true
		end
	elseif rank:sub(-1) == "-" then
		for i = rankPos, #self.ranks do
			self.ranksi = true
		end
	else
		self.ranksrankPos = true
	end
end
function Config:setPeriod(period)
	local periodPos
	if period == p.flags.future then
		periodPos = 1
	elseif period == p.flags.current then
		periodPos = 2
	elseif period == p.flags.former then
		periodPos = 3
	else
		return
	end
	-- one of the period flags was given, check if another one was given before
	if not self.flagPeriod then
		self.periods = {false, false, false}  -- no other period flag given before, so unset periods
		self.flagPeriod = true                -- mark that a period flag was given
	end
	self.periodsperiodPos = true
end
function Config:qualifierMatches(claim, id, value)
	local qualifiers
	if claim.qualifiers then qualifiers = claim.qualifiersid end
	if qualifiers then
		for _, v in pairs(qualifiers) do
			if self:snakEqualsValue(v, value) then
				return true
			end
		end
	elseif value == "" then
		-- if the qualifier is not present then treat it the same as the special value 'novalue'
		return true
	end
	return false
end
function Config:rankMatches(rankPos)
	if self.bestRank then
		return (self.ranksrankPos and self.foundRank >= rankPos)
	else
		return self.ranksrankPos
	end
end
function Config:timeMatches(claim)
	local startTime = nil
	local startTimeY = nil
	local startTimeM = nil
	local startTimeD = nil
	local endTime = nil
	local endTimeY = nil
	local endTimeM = nil
	local endTimeD = nil
	if self.periods1 and self.periods2 and self.periods3 then
		-- any time
		return true
	end
	startTime = self:getSingleRawQualifier(claim, aliasesP.startTime)
	if startTime and startTime ~= "" and startTime ~= " " then
		startTimeY, startTimeM, startTimeD = parseDate(startTime)
	end
	endTime = self:getSingleRawQualifier(claim, aliasesP.endTime)
	if endTime and endTime ~= "" and endTime ~= " " then
		endTimeY, endTimeM, endTimeD = parseDate(endTime)
	end
	if startTimeY ~= nil and endTimeY ~= nil and datePrecedesDate(endTimeY, endTimeM, endTimeD, startTimeY, startTimeM, startTimeD) then
		-- invalidate end time if it precedes start time
		endTimeY = nil
		endTimeM = nil
		endTimeD = nil
	end
	if self.periods1 then
		-- future
		if startTimeY and datePrecedesDate(self.atDate1, self.atDate2, self.atDate3, startTimeY, startTimeM, startTimeD) then
			return true
		end
	end
	if self.periods2 then
		-- current
		if (startTimeY == nil or not datePrecedesDate(self.atDate1, self.atDate2, self.atDate3, startTimeY, startTimeM, startTimeD)) and
		   (endTimeY == nil or datePrecedesDate(self.atDate1, self.atDate2, self.atDate3, endTimeY, endTimeM, endTimeD)) then
			return true
		end
	end
	if self.periods3 then
		-- former
		if endTimeY and not datePrecedesDate(self.atDate1, self.atDate2, self.atDate3, endTimeY, endTimeM, endTimeD) then
			return true
		end
	end
	return false
end
function Config:processFlag(flag)
	if not flag then
		return false
	end
	if flag == p.flags.linked then
		self.curState.linked = true
		return true
	elseif flag == p.flags.raw then
		self.curState.rawValue = true
		if self.curState == self.statesparameters.reference then
			-- raw reference values end with periods and require a separator (other than none)
			self.separators"sep%r"1 = {" "}
		end
		return true
	elseif flag == p.flags.short then
		self.curState.shortName = true
		return true
	elseif flag == p.flags.multilanguage then
		self.curState.anyLanguage = true
		return true
	elseif flag == p.flags.unit then
		self.curState.unitOnly = true
		return true
	elseif flag == p.flags.mdy then
		self.mdyDate = true
		return true
	elseif flag == p.flags.single then
		self.singleClaim = true
		return true
	elseif flag == p.flags.sourced then
		self.sourcedOnly = true
		return true
	elseif flag == p.flags.edit then
		self.editable = true
		return true
	elseif flag == p.flags.editAtEnd then
		self.editable = true
		self.editAtEnd = true
		return true
	elseif flag == p.flags.best or flag:match('^'..p.flags.preferred..'+-?$') or flag:match('^'..p.flags.normal..'+-?$') or flag:match('^'..p.flags.deprecated..'+-?$') then
		self:setRank(flag)
		return true
	elseif flag == p.flags.future or flag == p.flags.current or flag == p.flags.former then
		self:setPeriod(flag)
		return true
	elseif flag == "" then
		-- ignore empty flags and carry on
		return true
	else
		return false
	end
end
function Config:processFlagOrCommand(flag)
	local param = ""
	if not flag then
		return false
	end
	if flag == p.claimCommands.property or flag == p.claimCommands.properties then
		param = parameters.property
	elseif flag == p.claimCommands.qualifier or flag == p.claimCommands.qualifiers then
		self.states.qualifiersCount = self.states.qualifiersCount + 1
		param = parameters.qualifier .. self.states.qualifiersCount
		self.separators"sep"..param = {copyTable(defaultSeparators"sep%q\\d")}
	elseif flag == p.claimCommands.reference or flag == p.claimCommands.references then
		param = parameters.reference
	else
		return self:processFlag(flag)
	end
	if self.statesparam then
		return false
	end
	-- create a new state for each command
	self.statesparam = State:new(self, param)
	-- use "%x" as the general parameter name
	self.statesparam.parsedFormat = parseFormat(parameters.general)  -- will be overwritten for param=="%p"
	-- set the separator
	self.statesparam.separator = self.separators"sep"..param  -- will be nil for param=="%p", which will be set separately
	if flag == p.claimCommands.property or flag == p.claimCommands.qualifier or flag == p.claimCommands.reference then
		self.statesparam.singleValue = true
	end
	self.curState = self.statesparam
	return true
end
function Config:processSeparators(args)
	local sep
	for i, v in pairs(self.separators) do
		if argsi then
			sep = replaceSpecialChars(argsi)
			if sep ~= "" then
				self.separatorsi1 = {sep}
			else
				self.separatorsi1 = nil
			end
		end
	end
end
function Config:setFormatAndSeparators(state, parsedFormat)
	state.parsedFormat = parsedFormat
	state.separator = self.separators"sep"
	state.movSeparator = self.separators"sep"..parameters.separator
	state.puncMark = self.separators"punc"
end
-- determines if a claim has references by prefetching them from the claim using getReferences,
-- which applies some filtering that determines if a reference is actually returned,
-- and caches the references for later use
function State:isSourced(claim)
	self.conf.prefetchedRefs = self:getReferences(claim)
	return (#self.conf.prefetchedRefs > 0)
end
function State:resetCaches()
	-- any prefetched references of the previous claim must not be used
	self.conf.prefetchedRefs = nil
end
function State:claimMatches(claim)
	local matches, rankPos
	-- first of all, reset any cached values used for the previous claim
	self:resetCaches()
	-- if a property value was given, check if it matches the claim's property value
	if self.conf.propertyValue then
		matches = self.conf:snakEqualsValue(claim.mainsnak, self.conf.propertyValue)
	else
		matches = true
	end
	-- if any qualifier values were given, check if each matches one of the claim's qualifier values
	for i, v in pairs(self.conf.qualifierIDsAndValues) do
		matches = (matches and self.conf:qualifierMatches(claim, i, v))
	end
	-- check if the claim's rank and time period match
	rankPos = rankTableclaim.rank or 4
	matches = (matches and self.conf:rankMatches(rankPos) and self.conf:timeMatches(claim))
	-- if only claims with references must be returned, check if this one has any
	if self.conf.sourcedOnly then
		matches = (matches and self:isSourced(claim))  -- prefetches and caches references
	end
	return matches, rankPos
end
function State:out()
	local result  -- collection of arrays with value objects
	local valuesArray  -- array with value objects
	local sep = nil  -- value object
	local out = {}  -- array with value objects
	local function walk(formatTable, result)
		local valuesArray = {}  -- array with value objects
		for i, v in pairs(formatTable.req) do
			if not resulti or not resulti1 then
				-- we've got no result for a parameter that is required on this level,
				-- so skip this level (and its children) by returning an empty result
				return {}
			end
		end
		for _, v in ipairs(formatTable) do
			if v.param then
				valuesArray = mergeArrays(valuesArray, resultv.str)
			elseif v.str ~= "" then
				valuesArray#valuesArray + 1 = {v.str}
			end
			if v.child then
				valuesArray = mergeArrays(valuesArray, walk(v.child, result))
			end
		end
		return valuesArray
	end
	-- iterate through the results from back to front, so that we know when to add separators
	for i = #self.results, 1, -1 do
		result = self.resultsi
		-- if there is already some output, then add the separators
		if #out > 0 then
			sep = self.separator1  -- fixed separator
			resultparameters.separator = {self.movSeparator1}  -- movable separator
		else
			sep = nil
			resultparameters.separator = {self.puncMark1}  -- optional punctuation mark
		end
		valuesArray = walk(self.parsedFormat, result)
		if #valuesArray > 0 then
			if sep then
				valuesArray#valuesArray + 1 = sep
			end
			out = mergeArrays(valuesArray, out)
		end
	end
	-- reset state before next iteration
	self.results = {}
	return out
end
-- level 1 hook
function State:getProperty(claim)
	local value = {self:getValue(claim.mainsnak)}  -- create one value object
	if #value > 0 then
		return {value}  -- wrap the value object in an array and return it
	else
		return {}  -- return empty array if there was no value
	end
end
-- level 1 hook
function State:getQualifiers(claim, param)
	local qualifiers
	if claim.qualifiers then qualifiers = claim.qualifiersself.conf.qualifierIDsparam end
	if qualifiers then
		-- iterate through claim's qualifier statements to collect their values;
		-- return array with multiple value objects
		return self.conf.statesparam:iterate(qualifiers, {parameters.general = hookNamesparameters.qualifier.."\\d"2, count = 1})  -- pass qualifier state with level 2 hook
	else
		return {}  -- return empty array
	end
end
-- level 2 hook
function State:getQualifier(snak)
	local value = {self:getValue(snak)}  -- create one value object
	if #value > 0 then
		return {value}  -- wrap the value object in an array and return it
	else
		return {}  -- return empty array if there was no value
	end
end
-- level 1 hook
function State:getAllQualifiers(claim, param, result, hooks)
	local out = {}  -- array with value objects
	local sep = self.conf.separators"sep"..parameters.qualifier1  -- value object
	-- iterate through the output of the separate "qualifier(s)" commands
	for i = 1, self.conf.states.qualifiersCount do
		-- if a hook has not been called yet, call it now
		if not resultparameters.qualifier..i then
			self:callHook(parameters.qualifier..i, hooks, claim, result)
		end
		-- if there is output for this particular "qualifier(s)" command, then add it
		if resultparameters.qualifier..i and resultparameters.qualifier..i1 then
			-- if there is already some output, then add the separator
			if #out > 0 and sep then
				out#out + 1 = sep
			end
			out = mergeArrays(out, resultparameters.qualifier..i)
		end
	end
	return out
end
-- level 1 hook
function State:getReferences(claim)
	if self.conf.prefetchedRefs then
		-- return references that have been prefetched by isSourced
		return self.conf.prefetchedRefs
	end
	if claim.references then
		-- iterate through claim's reference statements to collect their values;
		-- return array with multiple value objects
		return self.conf.statesparameters.reference:iterate(claim.references, {parameters.general = hookNamesparameters.reference2, count = 1})  -- pass reference state with level 2 hook
	else
		return {}  -- return empty array
	end
end
-- level 2 hook
function State:getReference(statement)
	local key, citeWeb, citeQ, label
	local params = {}
	local citeParams = {'web' = {}, 'q' = {}}
	local citeMismatch = {}
	local useCite = nil
	local useParams = nil
	local value = ""
	local ref = {}
	local referenceEmpty = true  -- will be set to false if at least one parameter is left unremoved
local numAuthorParameters = 0
local numAuthorNameStringParameters = 0
local tempLink
local additionalRefProperties = {}  -- will hold properties of the reference which are not in statement.snaks, namely backup title from "subject named as" and URL from an external ID
local wikidataPropertiesOfSource  -- will contain "Wikidata property" properties of the item in stated in, if any
	local version = 5  -- increment this each time the below logic is changed to avoid conflict errors
	if statement.snaks then
		-- don't include "imported from", which is added by a bot
		if statement.snaksaliasesP.importedFrom then
			statement.snaksaliasesP.importedFrom = nil
		end
		
		-- don't include "Wikimedia import URL"
		if statement.snaksaliasesP.wikimediaImportURL then
			statement.snaksaliasesP.wikimediaImportURL = nil
			-- don't include "retrieved" if no "referenceURL" is present,
			-- as "retrieved" probably belongs to "Wikimedia import URL"
			if statement.snaksaliasesP.retrieved and not statement.snaksaliasesP.referenceURL then
				statement.snaksaliasesP.retrieved = nil
			end
		end
		-- don't include "inferred from", which is added by a bot
		if statement.snaksaliasesP.inferredFrom then
			statement.snaksaliasesP.inferredFrom = nil
		end
		-- don't include "type of reference"
		if statement.snaksaliasesP.typeOfReference then
			statement.snaksaliasesP.typeOfReference = nil
		end
		-- don't include "image" to prevent littering
		if statement.snaksaliasesP.image then
			statement.snaksaliasesP.image = nil
		end
		-- don't include "language" if it is equal to the local one
		if self:getReferenceDetail(statement.snaks, aliasesP.language) == self.conf.langName then
			statement.snaksaliasesP.language = nil
		end

if statement.snaksaliasesP.statedIn and not statement.snaksaliasesP.referenceURL then
	-- "stated in" was given but "reference URL" was not.
	-- get "Wikidata property" properties from the item in "stated in"
	-- if any of the returned properties of the external-id datatype is in statement.snaks, generate a URL from it and use the URL in the reference
	
	-- find the "Wikidata property" properties in the item from "stated in"
	wikidataPropertiesOfSource = mw.text.split(p._properties{p.flags.raw, aliasesP.wikidataProperty, p.args.eid = self.conf:getValue(statement.snaksaliasesP.statedIn1, true, false)}, ", ", true)
	for i, wikidataPropertyOfSource in pairs(wikidataPropertiesOfSource) do
		if statement.snakswikidataPropertyOfSource and statement.snakswikidataPropertyOfSource1.datatype == "external-id" then
			tempLink = self.conf:getValue(statement.snakswikidataPropertyOfSource1, false, true)  -- not raw, linked
			if mw.ustring.match(tempLink, "^%%Z- %Z+%$") then  -- getValue returned a URL.
		    	additionalRefPropertiesaliasesP.referenceURL = mw.ustring.gsub(tempLink, "^%(%Z-) %Z+%$", "%1")  -- the URL is in wiki markup, so strip the square brackets and the display text
			    statement.snakswikidataPropertyOfSource = nil
			    break
			end
		end
	end
end

-- don't include "subject named as", but use it as the title when "title" is not present but a URL is
if statement.snaksaliasesP.subjectNamedAs then
	if not statement.snaksaliasesP.title and (statement.snaksaliasesP.referenceURL or additionalRefPropertiesaliasesP.referenceURL) then
		additionalRefPropertiesaliasesP.title = statement.snaksaliasesP.subjectNamedAs1.datavalue.value
	end
	statement.snaksaliasesP.subjectNamedAs = nil
end
		-- retrieve all the parameters
		for i in pairs(statement.snaks) do
			label = ""
			-- multiple authors may be given
			if i == aliasesP.author or i == aliasesP.authorNameString then
				paramsi = self:getReferenceDetails(statement.snaks, i, false, self.linked, true)  -- link = true/false, anyLang = true
			else
				paramsi = {self:getReferenceDetail(statement.snaks, i, false, (self.linked or (i == aliasesP.statedIn)) and (statement.snaksi1.datatype ~= 'url'), true)}  -- link = true/false, anyLang = true
			end
			if #paramsi == 0 then
				paramsi = nil
			else
				referenceEmpty = false
				
				if statement.snaksi1.datatype == 'external-id' then
					key = "external-id"
					label = self.conf:getLabel(i)
					if label ~= "" then
						label = label .. " "
					end
				else
					key = i
				end
				-- add the parameter to each matching type of citation
				for j in pairs(citeParams) do
					-- do so if there was no mismatch with a previous parameter
					if not citeMismatchj then
						-- check if this parameter is not mismatching itself
						if i18n'cite'jkey then
							-- continue if an option is available in the corresponding cite template
							if i18n'cite'jkey ~= "" then
-- handle non-author properties (and author properties ("author" and "author name string"), if they don't use the same template parameter)
if (i ~= aliasesP.author and i ~= aliasesP.authorNameString) or (i18n'cite'jaliasesP.author ~= i18n'cite'jaliasesP.authorNameString) then
								    citeParamsji18n'cite'jkey = label .. paramsi1
-- to avoid problems with non-author multiple parameters (if existent), the following old code is retained
								    for k=2, #paramsi do
citeParamsji18n'cite'jkey..k = label .. paramsik
							    	end
-- handle "author" and "author name string" specially if they use the same template parameter
elseif i == aliasesP.author or i == aliasesP.authorNameString then
if paramsaliasesP.author ~= nil then
numAuthorParameters = #paramsaliasesP.author
else
numAuthorParameters = 0
end
if paramsaliasesP.authorNameString ~= nil then
numAuthorNameStringParameters = #paramsaliasesP.authorNameString
else
numAuthorNameStringParameters = 0
end
-- execute only if both "author" and "author name string" satisfy this condition: the property is both in params and in statement.snaks or it is neither in params nor in statement.snaks
-- reason: parameters are added to params each iteration of the loop, not before the loop
if ((statement.snaksaliasesP.author == nil) == (numAuthorParameters == 0)) and ((statement.snaksaliasesP.authorNameString == nil) == (numAuthorNameStringParameters == 0)) then
for k=1, numAuthorParameters + numAuthorNameStringParameters do
if k " .. errorText("malformed-reference") .. ""
		end
	
		if value ~= "" then
			value = {value}  -- create one value object
			if not self.rawValue then
				-- this should become a  tag, so save the reference's hash for later
				value.refHash = "wikidata-" .. statement.hash .. "-v" .. (tonumber(i18n'citeversion') + version)
			end
			ref = {value}  -- wrap the value object in an array
		end
	end
	return ref
end
-- gets a detail of one particular type for a reference
function State:getReferenceDetail(snaks, dType, raw, link, anyLang)
	local switchLang = anyLang
	local value = nil
	if not snaksdType then
		return nil
	end
	-- if anyLang, first try the local language and otherwise any language
	repeat
		for _, v in ipairs(snaksdType) do
			value = self.conf:getValue(v, raw, link, false, anyLang and not switchLang, false, true)  -- noSpecial = true
			if value then
				break
			end
		end
		if value or not anyLang then
			break
		end
		switchLang = not switchLang
	until anyLang and switchLang
	return value
end
-- gets the details of one particular type for a reference
function State:getReferenceDetails(snaks, dType, raw, link, anyLang)
	local values = {}
	if not snaksdType then
		return {}
	end
	for _, v in ipairs(snaksdType) do
		-- if nil is returned then it will not be added to the table
		values#values + 1 = self.conf:getValue(v, raw, link, false, anyLang, false, true)  -- noSpecial = true
	end
	return values
end
-- level 1 hook
function State:getAlias(object)
	local value = object.value
	local title = nil
	if value and self.linked then
		if self.conf.entityID:sub(1,1) == "Q" then
			title = mw.wikibase.getSitelink(self.conf.entityID)
		elseif self.conf.entityID:sub(1,1) == "P" then
			title = "d:Property:" .. self.conf.entityID
		end
		if title then
			value = buildWikilink(title, value)
		end
	end
	value = {value}  -- create one value object
	if #value > 0 then
		return {value}  -- wrap the value object in an array and return it
	else
		return {}  -- return empty array if there was no value
	end
end
-- level 1 hook
function State:getBadge(value)
	value = self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)
	if value == "" then
		value = nil
	end
	value = {value}  -- create one value object
	if #value > 0 then
		return {value}  -- wrap the value object in an array and return it
	else
		return {}  -- return empty array if there was no value
	end
end
function State:callHook(param, hooks, statement, result)
	local valuesArray, refHash
	-- call a parameter's hook if it has been defined and if it has not been called before
	if not resultparam and hooksparam then
		valuesArray = selfhooksparam(self, statement, param, result, hooks)  -- array with value objects
		-- add to the result
		if #valuesArray > 0 then
			resultparam = valuesArray
			result.count = result.count + 1
		else
			resultparam = {}  -- an empty array to indicate that we've tried this hook already
			return true  -- miss == true
		end
	end
	return false
end
-- iterate through claims, claim's qualifiers or claim's references to collect values
function State:iterate(statements, hooks, matchHook)
	matchHook = matchHook or alwaysTrue
	local matches = false
	local rankPos = nil
	local result, gotRequired
	for _, v in ipairs(statements) do
		-- rankPos will be nil for non-claim statements (e.g. qualifiers, references, etc.)
		matches, rankPos = matchHook(self, v)
		if matches then
			result = {count = 0}  -- collection of arrays with value objects
			local function walk(formatTable)
				local miss
				for i2, v2 in pairs(formatTable.req) do
					-- call a hook, adding its return value to the result
					miss = self:callHook(i2, hooks, v, result)
					if miss then
						-- we miss a required value for this level, so return false
						return false
					end
					if result.count == hooks.count then
						-- we're done if all hooks have been called;
						-- returning at this point breaks the loop
						return true
					end
				end
				for _, v2 in ipairs(formatTable) do
					if result.count == hooks.count then
						-- we're done if all hooks have been called;
						-- returning at this point prevents further childs from being processed
						return true
					end
					if v2.child then
						walk(v2.child)
					end
				end
				return true
			end
			gotRequired = walk(self.parsedFormat)
			-- only append the result if we got values for all required parameters on the root level
			if gotRequired then
				-- if we have a rankPos (only with matchHook() for complete claims), then update the foundRank
				if rankPos and self.conf.foundRank > rankPos then
					self.conf.foundRank = rankPos
				end
				-- append the result
				self.results#self.results + 1 = result
				-- break if we only need a single value
				if self.singleValue then
					break
				end
			end
		end
	end
	return self:out()
end
local function getEntityId(arg, eid, page, allowOmitPropPrefix)
	local id = nil
	local prop = nil
	if arg then
		if arg:sub(1,1) == ":" then
			page = arg
			eid = nil
		elseif arg:sub(1,1):upper() == "Q" or arg:sub(1,9):lower() == "property:" or allowOmitPropPrefix then
			eid = arg
			page = nil
		else
			prop = arg
		end
	end
	if eid then
		if eid:sub(1,9):lower() == "property:" then
			id = replaceAlias(mw.text.trim(eid:sub(10)))
			if id:sub(1,1):upper() ~= "P" then
				id = ""
			end
		else
			id = replaceAlias(eid)
		end
	elseif page then
		if page:sub(1,1) == ":" then
			page = mw.text.trim(page:sub(2))
		end
		id = mw.wikibase.getEntityIdForTitle(page) or ""
	end
	if not id then
		id = mw.wikibase.getEntityIdForCurrentPage() or ""
	end
	id = id:upper()
	if not mw.wikibase.isValidEntityId(id) then
		id = ""
	end
	return id, prop
end
local function nextArg(args)
	local arg = argsargs.pointer
	if arg then
		args.pointer = args.pointer + 1
		return mw.text.trim(arg)
	else
		return nil
	end
end
local function claimCommand(args, funcName)
	local cfg = Config:new()
	cfg:processFlagOrCommand(funcName)  -- process first command (== function name)
	local lastArg, parsedFormat, formatParams, claims, value
	local hooks = {count = 0}
	-- set the date if given;
	-- must come BEFORE processing the flags
	if argsp.args.date then
		cfg.atDate = {parseDate(argsp.args.date)}
		cfg.periods = {false, true, false}  -- change default time constraint to 'current'
	end
	-- process flags and commands
	repeat
		lastArg = nextArg(args)
	until not cfg:processFlagOrCommand(lastArg)
	-- get the entity ID from either the positional argument, the eid argument or the page argument
	cfg.entityID, cfg.propertyID = getEntityId(lastArg, argsp.args.eid, argsp.args.page)
	if cfg.entityID == "" then
		return ""  -- we cannot continue without a valid entity ID
	end
	cfg.entity = mw.wikibase.getEntity(cfg.entityID)
	if not cfg.propertyID then
		cfg.propertyID = nextArg(args)
	end
	cfg.propertyID = replaceAlias(cfg.propertyID)
	if not cfg.entity or not cfg.propertyID then
		return ""  -- we cannot continue without an entity or a property ID
	end
	cfg.propertyID = cfg.propertyID:upper()
	if not cfg.entity.claims or not cfg.entity.claimscfg.propertyID then
		return ""  -- there is no use to continue without any claims
	end
	claims = cfg.entity.claimscfg.propertyID
	if cfg.states.qualifiersCount > 0 then
		-- do further processing if "qualifier(s)" command was given
		if #args - args.pointer + 1 > cfg.states.qualifiersCount then
			-- claim ID or literal value has been given
			cfg.propertyValue = nextArg(args)
		end
		for i = 1, cfg.states.qualifiersCount do
			-- check if given qualifier ID is an alias and add it
			cfg.qualifierIDsparameters.qualifier..i = replaceAlias(nextArg(args) or ""):upper()
		end
	elseif cfg.statesparameters.reference then
		-- do further processing if "reference(s)" command was given
		cfg.propertyValue = nextArg(args)
	end
	-- check for special property value 'somevalue' or 'novalue'
	if cfg.propertyValue then
		cfg.propertyValue = replaceSpecialChars(cfg.propertyValue)
		if cfg.propertyValue ~= "" and mw.text.trim(cfg.propertyValue) == "" then
			cfg.propertyValue = " "  -- single space represents 'somevalue', whereas empty string represents 'novalue'
		else
			cfg.propertyValue = mw.text.trim(cfg.propertyValue)
		end
	end
	-- parse the desired format, or choose an appropriate format
	if args"format" then
		parsedFormat, formatParams = parseFormat(args"format")
	elseif cfg.states.qualifiersCount > 0 then  -- "qualifier(s)" command given
		if cfg.statesparameters.property then  -- "propert(y|ies)" command given
			parsedFormat, formatParams = parseFormat(formats.propertyWithQualifier)
		else
			parsedFormat, formatParams = parseFormat(formats.qualifier)
		end
	elseif cfg.statesparameters.property then  -- "propert(y|ies)" command given
		parsedFormat, formatParams = parseFormat(formats.property)
	else  -- "reference(s)" command given
		parsedFormat, formatParams = parseFormat(formats.reference)
	end
	-- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolon
	if cfg.states.qualifiersCount > 0 and not cfg.statesparameters.property then
		cfg.separators"sep"..parameters.separator1 = {";"}
	end
	-- if only "reference(s)" has been given, set the default separator to none (except when raw)
	if cfg.statesparameters.reference and not cfg.statesparameters.property and cfg.states.qualifiersCount == 0
	   and not cfg.statesparameters.reference.rawValue then
		cfg.separators"sep"1 = nil
	end
	-- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalent
	if cfg.states.qualifiersCount == 1 then
		cfg.separators"sep"..parameters.qualifier = cfg.separators"sep"..parameters.qualifier.."1"
	end
	-- process overridden separator values;
	-- must come AFTER tweaking the default separators
	cfg:processSeparators(args)
	-- define the hooks that should be called (getProperty, getQualifiers, getReferences);
	-- only define a hook if both its command ("propert(y|ies)", "reference(s)", "qualifier(s)") and its parameter ("%p", "%r", "%q1", "%q2", "%q3") have been given
	for i, v in pairs(cfg.states) do
		-- e.g. 'formatParams"%q1" or formatParams"%q"' to define hook even if "%q1" was not defined to be able to build a complete value for "%q"
		if formatParamsi or formatParamsi:sub(1, 2) then
			hooksi = getHookName(i, 1)
			hooks.count = hooks.count + 1
		end
	end
	-- the "%q" parameter is not attached to a state, but is a collection of the results of multiple states (attached to "%q1", "%q2", "%q3", ...);
	-- so if this parameter is given then this hook must be defined separately, but only if at least one "qualifier(s)" command has been given
	if formatParamsparameters.qualifier and cfg.states.qualifiersCount > 0 then
		hooksparameters.qualifier = getHookName(parameters.qualifier, 1)
		hooks.count = hooks.count + 1
	end
	-- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration;
	-- must come AFTER defining the hooks
	if not cfg.statesparameters.property then
		cfg.statesparameters.property = State:new(cfg, parameters.property)
		-- if the "single" flag has been given then this state should be equivalent to "property" (singular)
		if cfg.singleClaim then
			cfg.statesparameters.property.singleValue = true
		end
	end
	-- if the "sourced" flag has been given then create a state for "reference" if it doesn't exist yet, using default values,
	-- which must exist in order to be able to determine if a claim has any references;
	-- must come AFTER defining the hooks
	if cfg.sourcedOnly and not cfg.statesparameters.reference then
		cfg:processFlagOrCommand(p.claimCommands.reference)  -- use singular "reference" to minimize overhead
	end
	-- set the parsed format and the separators (and optional punctuation mark);
	-- must come AFTER creating the additonal states
	cfg:setFormatAndSeparators(cfg.statesparameters.property, parsedFormat)
	-- process qualifier matching values, analogous to cfg.propertyValue
	for i, v in pairs(args) do
		i = tostring(i)
		if i:match('^Pp%d+$') or aliasesPi then
			v = replaceSpecialChars(v)
			-- check for special qualifier value 'somevalue'
			if v ~= "" and mw.text.trim(v) == "" then
				v = " "  -- single space represents 'somevalue'
			end
			cfg.qualifierIDsAndValuesreplaceAlias(i):upper() = v
		end
	end
	-- first sort the claims on rank to pre-define the order of output (preferred first, then normal, then deprecated)
	claims = sortOnRank(claims)
	-- then iterate through the claims to collect values
	value = cfg:concatValues(cfg.statesparameters.property:iterate(claims, hooks, State.claimMatches))  -- pass property state with level 1 hooks and matchHook
	-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
	if cfg.editable and value ~= "" then
		value = value .. cfg:getEditIcon()
	end
	return value
end
local function generalCommand(args, funcName)
	local cfg = Config:new()
	cfg.curState = State:new(cfg)
	local lastArg
	local value = nil
	repeat
		lastArg = nextArg(args)
	until not cfg:processFlag(lastArg)
	-- get the entity ID from either the positional argument, the eid argument or the page argument
	cfg.entityID = getEntityId(lastArg, argsp.args.eid, argsp.args.page, true)
	if cfg.entityID == "" or not mw.wikibase.entityExists(cfg.entityID) then
		return ""  -- we cannot continue without an entity
	end
	-- serve according to the given command
	if funcName == p.generalCommands.label then
		value = cfg:getLabel(cfg.entityID, cfg.curState.rawValue, cfg.curState.linked, cfg.curState.shortName)
	elseif funcName == p.generalCommands.title then
		cfg.inSitelinks = true
		if cfg.entityID:sub(1,1) == "Q" then
			value = mw.wikibase.getSitelink(cfg.entityID)
		end
		if cfg.curState.linked and value then
			value = buildWikilink(value)
		end
	elseif funcName == p.generalCommands.description then
		value = mw.wikibase.getDescription(cfg.entityID)
	else
		local parsedFormat, formatParams
		local hooks = {count = 0}
		cfg.entity = mw.wikibase.getEntity(cfg.entityID)
		if funcName == p.generalCommands.alias or funcName == p.generalCommands.badge then
			cfg.curState.singleValue = true
		end
		if funcName == p.generalCommands.alias or funcName == p.generalCommands.aliases then
			if not cfg.entity.aliases or not cfg.entity.aliasescfg.langCode then
				return ""  -- there is no use to continue without any aliasses
			end
			local aliases = cfg.entity.aliasescfg.langCode
			-- parse the desired format, or parse the default aliases format
			if args"format" then
				parsedFormat, formatParams = parseFormat(args"format")
			else
				parsedFormat, formatParams = parseFormat(formats.alias)
			end
			-- process overridden separator values;
			-- must come AFTER tweaking the default separators
			cfg:processSeparators(args)
			-- define the hook that should be called (getAlias);
			-- only define the hook if the parameter ("%a") has been given
			if formatParamsparameters.alias then
				hooksparameters.alias = getHookName(parameters.alias, 1)
				hooks.count = hooks.count + 1
			end
			-- set the parsed format and the separators (and optional punctuation mark)
			cfg:setFormatAndSeparators(cfg.curState, parsedFormat)
			-- iterate to collect values
			value = cfg:concatValues(cfg.curState:iterate(aliases, hooks))
		elseif funcName == p.generalCommands.badge or funcName == p.generalCommands.badges then
			if not cfg.entity.sitelinks or not cfg.entity.sitelinkscfg.siteID or not cfg.entity.sitelinkscfg.siteID.badges then
				return ""  -- there is no use to continue without any badges
			end
			local badges = cfg.entity.sitelinkscfg.siteID.badges
			cfg.inSitelinks = true
			-- parse the desired format, or parse the default aliases format
			if args"format" then
				parsedFormat, formatParams = parseFormat(args"format")
			else
				parsedFormat, formatParams = parseFormat(formats.badge)
			end
			-- process overridden separator values;
			-- must come AFTER tweaking the default separators
			cfg:processSeparators(args)
			-- define the hook that should be called (getBadge);
			-- only define the hook if the parameter ("%b") has been given
			if formatParamsparameters.badge then
				hooksparameters.badge = getHookName(parameters.badge, 1)
				hooks.count = hooks.count + 1
			end
			-- set the parsed format and the separators (and optional punctuation mark)
			cfg:setFormatAndSeparators(cfg.curState, parsedFormat)
			-- iterate to collect values
			value = cfg:concatValues(cfg.curState:iterate(badges, hooks))
		end
	end
	value = value or ""
	if cfg.editable and value ~= "" then
		-- if desired, add a clickable icon that may be used to edit the returned value on Wikidata
		value = value .. cfg:getEditIcon()
	end
	return value
end
-- modules that include this module should call the functions with an underscore prepended, e.g.: p._property(args)
local function establishCommands(commandList, commandFunc)
	for _, commandName in pairs(commandList) do
		local function wikitextWrapper(frame)
			local args = copyTable(frame.args)
			args.pointer = 1
			loadI18n(aliasesP, frame)
			return commandFunc(args, commandName)
		end
		pcommandName = wikitextWrapper
		local function luaWrapper(args)
			args = copyTable(args)
			args.pointer = 1
			loadI18n(aliasesP)
			return commandFunc(args, commandName)
		end
		p"_" .. commandName = luaWrapper
	end
end
establishCommands(p.claimCommands, claimCommand)
establishCommands(p.generalCommands, generalCommand)
-- main function that is supposed to be used by wrapper templates
function p.main(frame)
	if not mw.wikibase then return nil end
	local f, args
	loadI18n(aliasesP, frame)
	-- get the parent frame to take the arguments that were passed to the wrapper template
	frame = frame:getParent() or frame
	if not frame.args1 then
		throwError("no-function-specified")
	end
	f = mw.text.trim(frame.args1)
	if f == "main" then
		throwError("main-called-twice")
	end
	assert(p"_"..f, errorText('no-such-function', f))
	-- copy arguments from immutable to mutable table
	args = copyTable(frame.args)
	-- remove the function name from the list
	table.remove(args, 1)
	return p"_"..f(args)
end
return p