mic_none

Module:Sandbox/Thayts/Wd Source: en.wikipedia.org/wiki/Module:Sandbox/Thayts/Wd

-- Original module located at [[:en:Module:Wd]] and [[:en:Module:Wd/i18n]].

local p = {}
local arg = ...
local i18n

--==-- Public declarations and initializations --==--

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",
	number        = "number",
	-------------
	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",
	sort = "sort"
}

--==-- Public constants --==--

-- An Ogham space that, just like a normal space, is not accepted by Wikidata as a valid single-character string value,
-- but which does not get trimmed as leading/trailing whitespace when passed in an invocation's named argument value.
-- This allows it to be used as a special character representing the special value 'somevalue' unambiguously.
-- Another advantage of this character is that it is usually visible as a dash instead of whitespace.
p.SOMEVALUE = " "
p.JULIAN = "Julian"

--==-- Private constants --==--

local NB_SPACE     = " "
local ENC_PIPE     = "|"
local SLASH        = "/"
local LAT_DIR_N_EN = "N"
local LAT_DIR_S_EN = "S"
local LON_DIR_E_EN = "E"
local LON_DIR_W_EN = "W"
local PROP         = "prop"
local RANK         = "rank"
local CLAIM        = "_claim"
local REFERENCE    = "_reference"
local UNIT         = "_unit"
local UNKNOWN      = "_unknown"

--==-- Private declarations and initializations --==--

local aliasesP = {
	coord                   = "P625",
	-----------------------
	image                   = "P18",
	author                  = "P50",
	publisher               = "P123",
	importedFrom            = "P143",
	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"
}

local aliasesQ = {
	julianCalendar          = "Q11184",
	percentage              = "Q11229",
	commonEra               = "Q208141",
	prolepticJulianCalendar = "Q1985786",
	citeWeb                 = "Q5637226",
	citeQ                   = "Q22321052"
}

local parameters = {
	property  = "p",
	qualifier = "q",
	reference = "r",
	alias     = "a",
	badge     = "b",
	separator = "s"
}

local formats = {
	property              = "%p[%s][%r]",
	qualifier             = "%q[%s][%r]",
	reference             = "%r",
	propertyWithQualifier = "%p[ <span style=\"font-size:85\\%\">(%q)</span>][%s][%r]"
}

local hookNames = {            -- {level_1, level_2}
	[parameters.property]       = {"getProperty"},
	[parameters.reference]      = {"getReferences", "getReference"},
	[parameters.qualifier]      = {"getAllQualifiers"},
	[parameters.qualifier.."0"] = {"getQualifiers", "getQualifier"},
	[parameters.alias]          = {"getAlias"},
	[parameters.badge]          = {"getBadge"},
	[parameters.separator]      = {"getSeparator"}
}

local defaultSeparators = {
	["sep"]    = " ",
	["sep%s"]  = ",",
	["sep%q"]  = "; ",
	["sep%q0"] = ", ",
	["sep%r"]  = "",  -- none
	["punc"]   = ""   -- none
}

local rankTable = {
	["preferred"]  = {1},
	["normal"]     = {2},
	["deprecated"] = {3}
}

--==-- Private functions --==--

-- used to merge output arrays together;
-- note that it currently mutates the first input array
local function mergeArrays(a1, a2)
	for i = 1, #a2 do
		a1[#a1 + 1] = a2[i]
	end

	return a1
end

-- used to make frame.args mutable, to replace #frame.args (which is always 0)
-- with the actual amount and to simply copy tables;
-- does a shallow copy, so nested tables are not copied but linked
local function copyTable(tIn)
	if not tIn then
		return nil
	end

	local tOut = {}

	for i, v in pairs(tIn) do
		tOut[i] = v
	end

	return tOut
end

-- implementation of pairs that skips numeric keys
local function npairs(t)
	return function(t, k)
		local v

		repeat
			k, v = next(t, k)
		until k == nil or type(k) ~= 'number'

		return k, v
	end, t , nil
end

local function toString(object, insideRef, refs)
	local mt, value

	insideRef = insideRef or false
	refs = refs or {{}}

	if not object then
		refs.squashed = false
		return ""
	end

	mt = getmetatable(object)

	if mt.sep then
		local array = {}

		for _, obj in ipairs(object) do
			local ref = refs[1]

			if not insideRef and array[1] and mt.sep[1] ~= "" then
				refs[1] = {}
			end

			value = toString(obj, insideRef, refs)

			if value ~= "" or (refs.squashed and not array[1]) then
				array[#array + 1] = value
			else
				refs[1] = ref
			end
		end

		value = table.concat(array, mt.sep[1])
	else
		if mt.hash then
			if refs[1][mt.hash] then
				refs.squashed = true
				return ""
			end

			insideRef = true
		end

		if mt.format then
			local ref, squashed, array

			local function processFormat(format)
				local array = {}
				local params = {}

				-- see if there are required parameters to expand
				if format.req then

					-- before expanding any parameters, check that none of them is nil
					for i, _ in pairs(format.req) do
						if not object[i] then
							return array  -- empty
						end
					end
				end

				-- process the format and childs (+1 is needed to process trailing childs)
				for i = 1, #format + 1 do
					if format.childs and format.childs[i] then
						for _, child in ipairs(format.childs[i]) do
							local ref = copyTable(refs[1])
							local squashed = refs.squashed

							local childArray = processFormat(child)

							if not childArray[1] then
								refs[1] = ref
								refs.squashed = squashed
							else
								mergeArrays(array, childArray)
							end
						end
					end

					if format.params and format.params[i] then
						array[#array + 1] = toString(object[format[i]], insideRef, refs)

						if array[#array] == "" and not refs.squashed then
							return {}
						end
					elseif format[i] then
						array[#array + 1] = format[i]

						if not insideRef then
							refs[1] = {}
						end
					end
				end

				return array
			end

			ref = copyTable(refs[1])
			squashed = refs.squashed

			array = processFormat(mt.format)

			if not array[1] then
				refs[1] = ref
				refs.squashed = squashed
			end

			value = table.concat(array)
		else
			if mt.expand then
				local args = {}

				for i, j in npairs(object) do
					args[i] = toString(j, insideRef)
				end

				value = mw.getCurrentFrame():expandTemplate{title=mt.expand, args=args}
			elseif object.label then
				value = object.label
			else
				value = table.concat(object)
			end

			if not insideRef and not mt.hash and value ~= "" then
				refs[1] = {}
			end
		end

		if mt.sub then
			for i, j in pairs(mt.sub) do
				value = mw.ustring.gsub(value, i, j)
			end
		end

		if value ~= "" and mt.tag then
			value = mw.getCurrentFrame():extensionTag(mt.tag[1], value, mt.tag[2])

			if mt.hash then
				refs[1][mt.hash] = true
			end
		end

		refs.squashed = false
	end

	if mt.trail then
		value = value .. mt.trail

		if not insideRef then
			refs[1] = {}
			refs.squashed = false
		end
	end

	return value
end

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

local function replaceAlias(id)
	if aliasesP[id] then
		id = aliasesP[id]
	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['numeric']['decimal-mark'], 1)
end

local function padZeros(num, numDigits)
	local numZeros
	local negative = false

	if num < 0 then
		negative = true
		num = num * -1
	end

	num = tostring(num)
	numZeros = numDigits - num:len()

	for _ = 1, numZeros do
		num = "0"..num
	end

	if negative then
		num = "-"..num
	end

	return num
end

local function replaceSpecialChar(chr)
	if chr == '_' then
		-- replace underscores with spaces
		return ' '
	else
		return chr
	end
end

local function replaceSpecialChars(str)
	local chr
	local esc = false
	local strOut = ""

	for i = 1, #str do
		chr = str:sub(i,i)

		if not esc then
			if chr == '\\' then
				esc = true
			else
				strOut = strOut .. replaceSpecialChar(chr)
			end
		else
			strOut = strOut .. chr
			esc = false
		end
	end

	return strOut
end

local function isPropertyID(id) 
	return id:match('^P%d+$')
end

local function buildLink(target, label)
	local mt = {__tostring=toString}

	if not label then
		mt.format = {"[", target, "]"}
		return setmetatable({target, target=target, isWebTarget=true}, mt), mt
	else
		mt.format = {"[", target, " ", label, "]"}
		return setmetatable({label, target=target, isWebTarget=true}, mt), mt
	end
end

local function buildWikilink(target, label)
	local mt = {__tostring=toString}

	if not label or target == label then
		mt.format = {"[[", target, "]]"}
		return setmetatable({target, target=target}, mt), mt
	else
		mt.format = {"[[", target, "|", label, "]]"}
		return setmetatable({label, target=target}, mt), mt
	end
end

-- does a shallow copy of both the object and the metatable's format,
-- so nested tables are not copied but linked
local function copyValue(vIn)
	local vOut = copyTable(vIn)
	local mtIn = getmetatable(vIn)
	local mtOut = {format=copyTable(mtIn.format), __tostring=toString}
	return setmetatable(vOut, mtOut)
end

local function split(str, del, from)
	local i, j

	from = from or 1
	i, j = str:find(del, from)

	if i and j then
		return str:sub(1, i - 1), str:sub(j + 1), i, j
	end

	return str
end

local function urlEncode(url)
	local i, j, urlSplit, urlPath
	local urlPre = ""
	local count = 0
	local pathEnc = {}
	local delim = ""

	i, j = url:find("//", 1, true)

	-- check if a hostname is present
	if i == 1 or (i and url:sub(i - 1, i - 1) == ':') then
		urlSplit = {split(url, "[/?#]", j + 1)}
		urlPre = urlSplit[1]

		-- split the path from the hostname
		if urlSplit[2] then
			urlPath = url:sub(urlSplit[3], urlSplit[4]) .. urlSplit[2]
		else
			urlPath = ""
		end
	else
		urlPath = url  -- no hostname is present, so it's a path
	end

	-- encode each part of the path
	for part in mw.text.gsplit(urlPath, "[;/?:@&=+$,#]") do
		pathEnc[#pathEnc + 1] = delim
		pathEnc[#pathEnc + 1] = mw.uri.encode(mw.uri.decode(part, "PATH"), "PATH")
		count = count + #part + 1
		delim = urlPath:sub(count, count)
	end

	-- return the properly encoded URL
	return urlPre .. table.concat(pathEnc)
end

local function parseWikidataURL(url)
	local id

	if url:match('^http[s]?://') then
		id = ({split(url, "Q")})[2]

		if id then
			return "Q" .. id
		end
	end

	return nil
end

local function parseDate(dateStr, precision)
	precision = precision or "d"

	local i, j, index, ptr
	local parts = {nil, nil, nil}

	if dateStr == nil then
		return parts[1], parts[2], parts[3]  -- year, month, day
	end

	-- 'T' for snak values, '/' for outputs with '/Julian' attached
	i, j = dateStr:find("[T/]")

	if i then
		dateStr = dateStr:sub(1, i-1)
	end

	local from = 1

	if dateStr:sub(1,1) == "-" then
		-- this is a negative number, look further ahead
		from = 2
	end

	index = 1
	ptr = 1

	i, j = dateStr:find("-", from)

	if i then
		-- year
		parts[index] = tonumber(mw.ustring.gsub(dateStr:sub(ptr, i-1), "^\+(.+)$", "%1"), 10)  -- remove '+' sign (explicitly give base 10 to prevent error)

		if parts[index] == -0 then
			parts[index] = tonumber("0")  -- for some reason, 'parts[index] = 0' may actually store '-0', so parse from string instead
		end

		if precision == "y" then
			-- we're done
			return parts[1], parts[2], parts[3]  -- year, month, day
		end

		index = index + 1
		ptr = i + 1

		i, j = dateStr:find("-", ptr)

		if i then
			-- month
			parts[index] = tonumber(dateStr:sub(ptr, i-1), 10)

			if precision == "m" then
				-- we're done
				return parts[1], parts[2], parts[3]  -- year, month, day
			end

			index = index + 1
			ptr = i + 1
		end
	end

	if dateStr:sub(ptr) ~= "" then
		-- day if we have month, month if we have year, or year
		parts[index] = tonumber(dateStr:sub(ptr), 10)
	end

	return parts[1], parts[2], parts[3]  -- year, month, day
end

local function datePrecedesDate(dateA, dateB)
	if not dateA[1] or not dateB[1] then
		return nil
	end

	dateA[2] = dateA[2] or 1
	dateA[3] = dateA[3] or 1
	dateB[2] = dateB[2] or 1
	dateB[3] = dateB[3] or 1

	if dateA[1] < dateB[1] then
		return true
	end

	if dateA[1] > dateB[1] then
		return false
	end

	if dateA[2] < dateB[2] then
		return true
	end

	if dateA[2] > dateB[2] then
		return false
	end

	if dateA[3] < dateB[3] then
		return true
	end

	return false
end

local function newOptionalHook(hooks)
	return function(state, claim)
		state:callHooks(hooks, claim)

		return true
	end
end

local function newPersistHook(params)
	return function(state, claim)
		local param0

		if not state.resultsByStatement[claim][1] then
			local mt = copyTable(state.metaTable)
			mt.rank = claim.rank
			state.resultsByStatement[claim][1] = setmetatable({}, mt)

			local rankPos = (rankTable[claim.rank] or {})[1]

			if rankPos and rankPos < state.conf.foundRank then
				state.conf.foundRank = rankPos
			end
		end

		for param, _ in pairs(params) do
			if not state.resultsByStatement[claim][1][param] then
				state.resultsByStatement[claim][1][param] = state.resultsByStatement[claim][param]  -- persist result

				-- if we need to persist "q", then also persist "q1", "q2", etc.
				if param == parameters.qualifier then
					for i = 1, state.conf.qualifiersCount do
						param0 = param..i

						if state.resultsByStatement[claim][param0][1] then
							state.resultsByStatement[claim][1][param0] = state.resultsByStatement[claim][param0]
						end
					end
				end
			end
		end

		return true
	end
end

local function parseFormat(state, formatStr, i)
	local iNext, childHooks, param0
	local esc = false
	local param = 0
	local str = ""
	local hooks = {}
	local optionalHooks = {}
	local parsedFormat = {}
	local params = {}
	local childs = {}
	local req = {}

	i = i or 1

	local function flush()
		if str ~= "" then
			parsedFormat[#parsedFormat + 1] = str

			if param > 0 then
				req[str] = true
				params[#parsedFormat] = true

				if not state.hooksByParam[str] then
					if state.conf.statesByParam[str] or str == parameters.separator then
						state:newValueHook(str)
					elseif str == parameters.qualifier and state.conf.statesByParam[str.."1"] then
						state:newValueHook(str)

						for i = 1, state.conf.qualifiersCount do
							param0 = str..i

							if not state.hooksByParam[param0] then
								state:newValueHook(param0)
							end
						end
					end
				end

				hooks[#hooks + 1] = state.hooksByParam[str]
			end

			str = ""
		end

		param = 0
	end

	while i <= #formatStr do
		chr = formatStr:sub(i,i)

		if not esc then
			if chr == '\\' then
				if param > 0 then
					flush()
				end

				esc = true
			elseif chr == '%' then
				flush()
				param = 2
			elseif chr == '[' then
				flush()
				iNext = #parsedFormat + 1

				if not childs[iNext] then
					childs[iNext] = {}
				end

				childs[iNext][#childs[iNext] + 1], childHooks, i = parseFormat(state, formatStr, i + 1)

				if childHooks[1] then
					optionalHooks[#optionalHooks + 1] = newOptionalHook(childHooks)
				end
			elseif chr == ']' then
				break
			else
				if param > 1 then
					param = param - 1
				elseif param == 1 and not chr:match('%d') then
					flush()
				end

				str = str .. replaceSpecialChar(chr)
			end
		else
			str = str .. chr
			esc = false
		end

		i = i + 1
	end

	flush()

	if hooks[1] then
		hooks[#hooks + 1] = newPersistHook(req)
	end

	mergeArrays(hooks, optionalHooks)

	parsedFormat.params = params
	parsedFormat.childs = childs
	parsedFormat.req = req

	return parsedFormat, hooks, i
end

-- this function must stay in sync with the getValue function
local function parseValue(value, datatype)
	if datatype == 'quantity' then
		return {tonumber(value)}
	elseif datatype == 'time' then
		local tail
		local dateValue = {}

		dateValue.len = 4  -- length used for comparing

		value, tail = split(value, SLASH)

		if tail and tail:lower() == p.JULIAN:lower() then
			dateValue[4] = p.JULIAN
		end

		if value:sub(1,1) == "-" then
			dateValue[1], value = split(value, "-", 2)
		else
			dateValue[1], value = split(value, "-")
		end

		dateValue[1] = tonumber(dateValue[1])

		if value then
			dateValue[2], value = split(value, "-")
			dateValue[2] = tonumber(dateValue[2])

			if value then
				dateValue[3] = tonumber(value)
			end
		end

		return dateValue
	elseif datatype == 'globecoordinate' then
		local part, partsIndex
		local coordValue = {}

		coordValue.len = 6  -- length used for comparing

		for i = 1, 4 do
			part, value = split(value, SLASH)
			coordValue[i] = tonumber(part)

			if not coordValue[i] or not value or i == 4 then
				coordValue[i] = nil
				partsIndex = i - 1
				break
			end
		end

		if part:upper() == LAT_DIR_S_EN then
			for i = 1, partsIndex do
				coordValue[i] = -coordValue[i]
			end
		end

		if value then
			partsIndex = partsIndex + 3

			for i = 4, partsIndex do
				part, value = split(value, SLASH)
				coordValue[i] = tonumber(part)

				if not coordValue[i] or not value then
					partsIndex = i - 1
					break
				end
			end

			if value and value:upper() == LON_DIR_W_EN then
				for i = 4, partsIndex do
					coordValue[i] = -coordValue[i]
				end
			end
		end

		return coordValue
	elseif datatype == 'wikibase-entityid' then
		return {value:sub(1,1):upper(), tonumber(value:sub(2))}
	end

	return {value}
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 = args[args.pointer]

	if arg then
		args.pointer = args.pointer + 1
		return mw.text.trim(arg)
	else
		return nil
	end
end

--==-- Classes --==--

local Config = {}

-- allows for recursive calls
function Config:new()
	local cfg = setmetatable({}, self)
	self.__index = self

	cfg.separators = {
		["sep"]   = {defaultSeparators["sep"]},
		["sep%q"] = {defaultSeparators["sep%q"]},
		["sep%r"] = {defaultSeparators["sep%r"]},
		["sep%s"] = setmetatable({defaultSeparators["sep%s"]}, {__tostring=toString}),
		["punc"]  = setmetatable({defaultSeparators["punc"]}, {__tostring=toString})
	}

	cfg.entity = nil
	cfg.entityID = nil
	cfg.propertyID = nil
	cfg.propertyValue = nil
	cfg.qualifierIDs = {}
	cfg.qualifierIDsAndValues = {}
	cfg.qualifiersCount = 0

	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.filterBeforeRank = 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.curTime = os.time()

	cfg.mdyDate = false
	cfg.singleClaim = false
	cfg.sourcedOnly = false
	cfg.editable = false
	cfg.editAtEnd = false

	cfg.inSitelinks = false

	cfg.emptyAllowed = 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.movSeparator = cfg.separators["sep%s"]
	cfg.puncMark = cfg.separators["punc"]

	cfg.statesByParam = {}
	cfg.statesByID = {}
	cfg.curState = nil

	cfg.sortKeys = {}

	return cfg
end

local State = {}

function State:new(cfg, level, param, id)
	local stt = setmetatable({}, self)
	self.__index = self

	stt.conf = cfg
	stt.level = level
	stt.param = param

	stt.linked = false
	stt.rawValue = false
	stt.shortName = false
	stt.anyLanguage = false
	stt.freeUnit = false
	stt.freeNumber = false
	stt.maxResults = 0  -- 0 means unlimited

	stt.metaTable = nil
	stt.results = {}
	stt.resultsByStatement = {}
	stt.references = {}
	stt.hooksByParam = {}
	stt.hooksByID = {}
	stt.valHooksByIdOrParam = {}
	stt.valHooks = {}
	stt.sortable = {}
	stt.sortPaths = {}
	stt.propState = nil

	if level and level > 1 then
		stt.hooks = {stt:newValueHook(param), stt.addToResults}
		stt.separator = cfg.separators["sep%"..param] or cfg.separators["sep"]  -- fall back to "sep" for getAlias and getBadge
		stt.resultsDatatype = nil
	else
		stt.hooks = {}
		stt.separator = cfg.separators["sep"]
		stt.resultsDatatype = {CLAIM}
	end

	if id then
		cfg:addToStatesByID(stt, id)
	elseif param then
		cfg.statesByParam[param] = stt
	end

	return stt
end

function Config:addToStatesByID(state, id)
	if not self.statesByID[id] then
		self.statesByID[id] = {}
	end

	self.statesByID[id][#self.statesByID[id] + 1] = state
end

-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short, emptyAllowed)
	local label
	local mt = {__tostring=toString}
	local value = setmetatable({}, mt)

	if not id then
		id = mw.wikibase.getEntityIdForCurrentPage()

		if not id then
			return value, mt  -- empty value
		end
	end

	id = id:upper()  -- just to be sure

	-- check if given id actually exists
	if not mw.wikibase.isValidEntityId(id) or not mw.wikibase.entityExists(id) then
		return value, mt  -- empty value
	end

	if raw then
		label = id
	else
		-- try short name first if requested
		if short then
			label = p.property{aliasesP.shortName, [p.args.eid] = id, format = "%"..parameters.property}  -- get short name

			if label == "" then
				label = nil
			end
		end

		-- get label
		if not label then
			label = mw.wikibase.getLabelByLang(id, self.langCode)
		end

		if not label and not emptyAllowed then
			return value, mt  -- empty value
		end

		value.label = label or ""
	end

	-- split id for potential numeric sorting
	value[1] = id:sub(1,1)
	value[2] = tonumber(id:sub(2))

	-- build a link if requested
	if link then
		if raw or value[1] == "P" then
			-- link to Wikidata if raw or if property (which has no sitelink)
			value.target = id

			if value[1] == "P" then
				value.target = "Property:" .. value.target
			end

			value.target = "d:" .. value.target
		else
			-- else, value[1] == "Q"
			value.target = mw.wikibase.getSitelink(id)
		end

		if value.target and label then
			mt.format = ({buildWikilink(value.target, label)})[2].format
		end
	end

	return value, mt
end

function Config:getEditIcon()
	local value = ""
	local prefix = ""
	local front = NB_SPACE
	local back = ""

	if self.entityID:sub(1,1) == "P" then
		prefix = "Property:"
	end

	if self.editAtEnd then
		front = '<span style="float:'

		if self.langObj:isRTL() then
			front = front .. 'left'
		else
			front = front .. 'right'
		end

		front = front .. '">'
		back = '</span>'
	end

	value = "[[File:OOjs UI icon edit-ltr-progressive.svg|frameless|text-top|10px|alt=" .. i18n['info']['edit-on-wikidata'] .. "|link=https://www.wikidata.orghttps://demo.azizisearch.com/lite/wikipedia/page/" .. 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['info']['edit-on-wikidata'] .. "]]"

	return front .. value .. back
end

function Config:convertUnit(unit, raw, link, short)
	local itemID
	local mt = {__tostring=toString}
	local value = setmetatable({}, mt)

	if unit == "" or unit == "1" then
		return value, mt
	end

	itemID = parseWikidataURL(unit)

	if itemID then
		if itemID == aliasesQ.percentage then
			value[1] = itemID:sub(1,1)
			value[2] = itemID:sub(2)

			if not raw then
				value.label = "%"
			elseif link then
				value.target = "d:" .. itemID
				mt.format = ({buildWikilink(value.target, itemID)})[2].format
			end
		else
			value, mt = self:getLabel(itemID, raw, link, short)

			if value.label then
				value.unitSep = NB_SPACE
			end
		end
	end

	return value, mt
end

function State:getValue(snak)
	return self.conf:getValue(snak, self.rawValue, self.linked, self.shortName, self.anyLanguage, self.freeUnit, self.freeNumber, false, self.conf.emptyAllowed, self.param:sub(1, 1))
end

-- returns a value object in the general form {raw_component_1, raw_component_2, ...} with metatable {format={str_component_1, str_component_2, ...}};
-- 'format' is the string representation of the value in unconcatenated form to exploit Lua's string internalization to reduce memory usage;
-- this function must stay in sync with the parseValue function
function Config:getValue(snak, raw, link, short, anyLang, freeUnit, freeNumber, noSpecial, emptyAllowed, param)
	local mt = {__tostring=toString}
	local value = setmetatable({}, mt)

	if snak.snaktype == 'value' then
		local datatype = snak.datavalue.type
		local subtype = snak.datatype
		local datavalue = snak.datavalue.value

		mt.datatype = {datatype}

		if datatype == 'string' then
			local datatypes = {datatype, subtype}

			value[1] = datavalue
			mt.datatype = datatypes

			if subtype == 'url' and link then
				-- create link explicitly
				if raw then
					-- will render as a linked number like [1]
					value, mt = buildLink(datavalue)
				else
					value, mt = buildLink(datavalue, datavalue)
				end

				mt.datatype = datatypes
				return value
			elseif subtype == 'commonsMedia' then
				if link then
					value, mt = buildWikilink("c:File:" .. datavalue, datavalue)
					mt.datatype = datatypes
				elseif not raw then
					mt.format = {"[[", "File:", datavalue, "]]"}
				end

				return value
			elseif subtype == 'geo-shape' and link then
				value, mt = buildWikilink("c:" .. datavalue, datavalue)
				mt.datatype = datatypes
				return value
			elseif subtype == 'math' and not raw then
				local attribute = nil

				if (param == parameters.property or (param == parameters.qualifier and self.propertyID == aliasesP.hasPart)) and snak.property == aliasesP.definingFormula then
					attribute = {qid = self.entityID}
				end

				mt.tag = {"math", attribute}
				return value
			elseif subtype == 'musical-notation' and not raw then
				mt.tag = {"score"}
				return value
			elseif subtype == 'external-id' and link then
				local url = p.property{aliasesP.formatterURL, [p.args.eid] = snak.property, format = "%"..parameters.property}  -- get formatter URL

				if url ~= "" then
					url = urlEncode(mw.ustring.gsub(url, "$1", datavalue))
					value, mt = buildLink(url, datavalue)
					mt.datatype = datatypes
				end

				return value
			else
				return value
			end
		elseif datatype == 'monolingualtext' then
			if anyLang or datavalue['language'] == self.langCode then
				value[1] = datavalue['text']
				value.language = datavalue['language']
			end

			return value
		elseif datatype == 'quantity' then
			local valueStr, unit

			if freeNumber or not freeUnit then
				-- get value and strip + signs from front
				valueStr = mw.ustring.gsub(datavalue['amount'], "^\+(.+)$", "%1")
				value[1] = tonumber(valueStr)

				-- assertion; we should always have a value
				if not value[1] then
					return value
				end

				if not raw then
					-- replace decimal mark based on locale
					valueStr = replaceDecimalMark(valueStr)

					-- add delimiters for readability
					valueStr = i18n.addDelimiters(valueStr)

					mt.format = {valueStr}
				end
			end

			if freeUnit or (not freeNumber and not raw) then
				local mtUnit

				unit, mtUnit = self:convertUnit(datavalue['unit'], raw, link, short)

				if freeUnit and not freeNumber then
					value = unit
					mt = mtUnit
					mt.datatype = {UNIT}
				elseif unit[1] then
					value[#value + 1] = unit[1]
					value[#value + 1] = unit[2]

					value.len = 1  -- (max) length used for sorting
					value.target = unit.target
					value.unitLabel = unit.label
					value.unitSep = unit.unitSep

					if raw then
						mt.format = {valueStr, SLASH}
						mergeArrays(mt.format, mtUnit.format or unit)
					else
						mt.format[#mt.format + 1] = unit.unitSep  -- may be nil
						mergeArrays(mt.format, mtUnit.format or {unit.label})
					end
				end
			end

			return value
		elseif datatype == 'time' then
			local y, m, d, p, yDiv, yRound, yFull, yRaw, mStr, ce, calendarID, target
			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 < 0 then
				sign = -1
				y = math.abs(y)
			end

			-- if precision is tens/hundreds/thousands/millions/billions of years
			if precision <= 8 then
				yDiv = y / yFactor

				-- if precision is tens/hundreds/thousands of years
				if precision >= 6 then
					mayAddCalendar = true

					if precision <= 7 then
						-- round centuries/millenniums up (e.g. 20th century or 3rd millennium)
						yRound = math.ceil(yDiv)

						-- take the first year of the century/millennium as the raw year
						-- (e.g. 1901 for 20th century or 2001 for 3rd millennium)
						yRaw = (yRound - 1) * yFactor + 1

						if not raw then
							if precision == 6 then
								suffix = i18n['datetime']['suffixes']['millennium']
							else
								suffix = i18n['datetime']['suffixes']['century']
							end

							suffix = i18n.getOrdinalSuffix(yRound) .. suffix
						else
							-- if not verbose, take the first year of the century/millennium
							yRound = yRaw
						end
					else
						-- precision == 8
						-- round decades down (e.g. 2010s)
						yRound = math.floor(yDiv) * yFactor
						yRaw = yRound

						if not raw then
							prefix = i18n['datetime']['prefixes']['decade-period']
							suffix = i18n['datetime']['suffixes']['decade-period']
						end
					end

					if sign < 0 then
						-- if BCE then compensate for "counting backwards"
						-- (e.g. -2019 for 2010s BCE, -2000 for 20th century BCE or -3000 for 3rd millennium BCE)
						yRaw = yRaw + yFactor - 1

						if raw then
							yRound = yRaw
						end
					end
				else
					local yReFactor, yReDiv, yReRound

					-- round to nearest for tens of thousands of years or more
					yRound = math.floor(yDiv + 0.5)

					if yRound == 0 then
						if precision <= 2 and y ~= 0 then
							yReFactor = 1e6
							yReDiv = y / yReFactor
							yReRound = math.floor(yReDiv + 0.5)

							if yReDiv == yReRound then
								-- change precision to millions of years only if we have a whole number of them
								precision = 3
								yFactor = yReFactor
								yRound = yReRound
							end
						end

						if yRound == 0 then
							-- otherwise, take the unrounded (original) number of years
							precision = 5
							yFactor = 1
							yRound = y
							mayAddCalendar = true
						end
					end

					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

					yRaw = yRound * yFactor

					if not raw then
						if precision == 3 then
							suffix = i18n['datetime']['suffixes']['million-years']
						elseif precision == 0 then
							suffix = i18n['datetime']['suffixes']['billion-years']
						else
							yRound = yRaw
							if yRound == 1 then
								suffix = i18n['datetime']['suffixes']['year']
							else
								suffix = i18n['datetime']['suffixes']['years']
							end
						end
					else
						yRound = yRaw
					end
				end
			else
				yRound = y
				yRaw = yRound
				mayAddCalendar = true
			end

			value[1] = yRaw * sign
			value[2] = m
			value[3] = d
			value.len = 3  -- (max) length used for sorting
			value.precision = precision
			mt.format = {}

			if not raw then
				if prefix ~= "" then
					mt.format[1] = prefix
				end

				if m then
					mStr = self.langObj:formatDate("F", "1-"..m.."-1")

					if d then
						if self.mdyDate then
							mt.format[#mt.format + 1] = mStr
							mt.format[#mt.format + 1] = " "
							mt.format[#mt.format + 1] = tostring(d)
							mt.format[#mt.format + 1] = ","
						else
							mt.format[#mt.format + 1] = tostring(d)
							mt.format[#mt.format + 1] = " "
							mt.format[#mt.format + 1] = mStr
						end
					else
						mt.format[#mt.format + 1] = mStr
					end

					mt.format[#mt.format + 1] = " "
				end

				mt.format[#mt.format + 1] = tostring(yRound)

				if suffix ~= "" then
					mt.format[#mt.format + 1] = suffix
				end

				if sign < 0 then
					ce = i18n['datetime']['BCE']
				elseif precision <= 5 then
					ce = i18n['datetime']['CE']
				end

				if ce then
					mt.format[#mt.format + 1] = " "

					if link then
						target = mw.wikibase.getSitelink(aliasesQ.commonEra)

						if target then
							mergeArrays(mt.format, ({buildWikilink(target, ce)})[2].format)
						else
							mt.format[#mt.format + 1] = ce
						end
					else
						mt.format[#mt.format + 1] = ce
					end
				end
			else
				mt.format[1] = padZeros(yRound * sign, 4)

				if m then
					mt.format[#mt.format + 1] = "-"
					mt.format[#mt.format + 1] = padZeros(m, 2)

					if d then
						mt.format[#mt.format + 1] = "-"
						mt.format[#mt.format + 1] = padZeros(d, 2)
					end
				end
			end

			calendarID = parseWikidataURL(datavalue['calendarmodel'])

			if calendarID and calendarID == aliasesQ.prolepticJulianCalendar then
				value[4] = p.JULIAN  -- as value.len == 3, this will not be taken into account while sorting

				if mayAddCalendar then
					if not raw then
						mt.format[#mt.format + 1] = " ("

						if link then
							target = mw.wikibase.getSitelink(aliasesQ.julianCalendar)

							if target then
								mergeArrays(mt.format, ({buildWikilink(target, i18n['datetime']['julian'])})[2].format)
							else
								mt.format[#mt.format + 1] = i18n['datetime']['julian']
							end
						else
							mt.format[#mt.format + 1] = i18n['datetime']['julian']
						end

						mt.format[#mt.format + 1] = ")"
					else
						mt.format[#mt.format + 1] = SLASH
						mt.format[#mt.format + 1] = p.JULIAN
					end
				end
			end

			return value
		elseif datatype == 'globecoordinate' then
			-- logic from https://github.com/DataValues/Geo (v4.0.1)

			local precision, unitsPerDegree, numDigits, strFormat, globe
			local latitude, latConv, latLink
			local longitude, lonConv, lonLink
			local latDirection, latDirectionN, latDirectionS, latDirectionEN
			local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN
			local latDegrees, latMinutes, latSeconds
			local lonDegrees, lonMinutes, lonSeconds
			local degSymbol, minSymbol, secSymbol, separator

			local latSign = 1
			local lonSign = 1

			local latFormat = {}
			local lonFormat = {}

			if not raw then
				latDirectionN = i18n['coord']['latitude-north']
				latDirectionS = i18n['coord']['latitude-south']
				lonDirectionE = i18n['coord']['longitude-east']
				lonDirectionW = i18n['coord']['longitude-west']

				degSymbol = i18n['coord']['degrees']
				minSymbol = i18n['coord']['minutes']
				secSymbol = i18n['coord']['seconds']
				separator = i18n['coord']['separator']
			else
				latDirectionN = LAT_DIR_N_EN
				latDirectionS = LAT_DIR_S_EN
				lonDirectionE = LON_DIR_E_EN
				lonDirectionW = LON_DIR_W_EN

				degSymbol = SLASH
				minSymbol = SLASH
				secSymbol = SLASH
				separator = SLASH
			end

			latitude = datavalue['latitude']
			longitude = datavalue['longitude']

			if latitude < 0 then
				latDirection = latDirectionS
				latDirectionEN = LAT_DIR_S_EN
				latSign = -1
				latitude = math.abs(latitude)
			else
				latDirection = latDirectionN
				latDirectionEN = LAT_DIR_N_EN
			end

			if longitude < 0 then
				lonDirection = lonDirectionW
				lonDirectionEN = LON_DIR_W_EN
				lonSign = -1
				longitude = math.abs(longitude)
			else
				lonDirection = lonDirectionE
				lonDirectionEN = LON_DIR_E_EN
			end

			precision = datavalue['precision']

			if not precision or precision <= 0 then
				precision = 1 / 3600  -- precision not set (correctly), set to arcsecond
			end

			-- remove insignificant detail
			latitude = math.floor(latitude / precision + 0.5) * precision
			longitude = math.floor(longitude / precision + 0.5) * precision

			if precision >= 1 - (1 / 60) and precision < 1 then
				precision = 1
			elseif precision >= (1 / 60) - (1 / 3600) and precision < (1 / 60) then
				precision = 1 / 60
			end

			if 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 <= 0 then
				numDigits = tonumber("0")  -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead
			end

			strFormat = "%." .. numDigits .. "f"

			if precision >= 1 then
				latDegrees = strFormat:format(latitude)
				lonDegrees = strFormat:format(longitude)
			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
						latFormat[5] = replaceDecimalMark(latSeconds)
						lonFormat[5] = replaceDecimalMark(lonSeconds)
					else
						latFormat[5] = latSeconds
						lonFormat[5] = lonSeconds
					end

					latFormat[6] = secSymbol
					lonFormat[6] = secSymbol

					value[3] = tonumber(latSeconds) * latSign
					value[6] = tonumber(lonSeconds) * lonSign
				end

				latDegrees = math.floor(latMinutes / 60)
				lonDegrees = math.floor(lonMinutes / 60)

				latMinutes = latMinutes - (latDegrees * 60)
				lonMinutes = lonMinutes - (lonDegrees * 60)

				if precision >= (1 / 60) then
					latMinutes = strFormat:format(latMinutes)
					lonMinutes = strFormat:format(lonMinutes)
				else
					latMinutes = tostring(latMinutes)
					lonMinutes = tostring(lonMinutes)
				end

				if not raw then
					latFormat[3] = replaceDecimalMark(latMinutes)
					lonFormat[3] = replaceDecimalMark(lonMinutes)
				else
					latFormat[3] = latMinutes
					lonFormat[3] = lonMinutes
				end

				latFormat[4] = minSymbol
				lonFormat[4] = minSymbol

				value[2] = tonumber(latMinutes) * latSign
				value[5] = tonumber(lonMinutes) * lonSign

				latDegrees = tostring(latDegrees)
				lonDegrees = tostring(lonDegrees)
			end

			if not raw then
				latFormat[1] = replaceDecimalMark(latDegrees)
				lonFormat[1] = replaceDecimalMark(lonDegrees)
			else
				latFormat[1] = latDegrees
				lonFormat[1] = lonDegrees
			end

			latFormat[2] = degSymbol
			lonFormat[2] = degSymbol

			value[1] = tonumber(latDegrees) * latSign
			value[4] = tonumber(lonDegrees) * lonSign
			value.len = 6  -- (max) length used for sorting

			latFormat[#latFormat + 1] = latDirection
			lonFormat[#lonFormat + 1] = lonDirection

			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.target = "https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."&params="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe
				value.isWebTarget = true

				mt.format = {"[", value.target, " "}
				mergeArrays(mt.format, latFormat)
				mt.format[#mt.format + 1] = separator
				mergeArrays(mt.format, lonFormat)
				mt.format[#mt.format + 1] = "]"
			else
				mt.format = latFormat
				mt.format[#mt.format + 1] = separator
				mergeArrays(mt.format, lonFormat)
			end

			return value
		elseif datatype == 'wikibase-entityid' then
			local itemID = datavalue['numeric-id']

			if subtype == 'wikibase-item' then
				itemID = "Q" .. itemID
			elseif subtype == 'wikibase-property' then
				itemID = "P" .. itemID
			else
				value[1] = errorText('unknown-data-type', subtype)
				mt.datatype = {UNKNOWN}
				mt.format = {'<strong class="error">', value[1], '</strong>'}
				return value
			end

			value, mt = self:getLabel(itemID, raw, link, short, emptyAllowed)
			mt.datatype = {datatype, subtype}

			return value
		else
			value[1] = errorText('unknown-data-type', datatype)
			mt.datatype = {UNKNOWN}
			mt.format = {'<strong class="error">', value[1], '</strong>'}
			return value
		end
	elseif snak.snaktype == 'somevalue' then
		if not noSpecial then
			value[1] = p.SOMEVALUE  -- one Ogham space represents 'somevalue'

			if not raw then
				mt.format = {i18n['values']['unknown']}
			end
		end

		mt.datatype = {snak.snaktype}
		return value
	elseif snak.snaktype == 'novalue' then
		if not noSpecial then
			value[1] = ""  -- empty string represents 'novalue'

			if not raw then
				mt.format = {i18n['values']['none']}
			end
		end

		mt.datatype = {snak.snaktype}
		return value
	else
		value[1] = errorText('unknown-data-type', snak.snaktype)
		mt.datatype = {UNKNOWN}
		mt.format = {'<strong class="error">', value[1], '</strong>'}
		return value
	end
end

function Config:getSingleRawQualifier(claim, qualifierID)
	local qualifiers

	if claim.qualifiers then qualifiers = claim.qualifiers[qualifierID] end

	if qualifiers and qualifiers[1] then
		return self:getValue(qualifiers[1], true)  -- raw = true
	else
		return nil
	end
end

function Config:snakEqualsValue(snak, value)
	local snakValue = self:getValue(snak, true)  -- raw = true
	local mt = getmetatable(snakValue)

	if mt.datatype[1] == UNKNOWN then
		return false
	end

	value = parseValue(value, mt.datatype[1])

	for i = 1, (value.len or #value) do
		if snakValue[i] ~= value[i] then
			return false
		end
	end

	return true
end

function Config:setRank(rank)
	local rankPos, step, to

	if rank == p.flags.best then
		self.bestRank = true
		self.flagBest = true  -- mark that 'best' flag was given
		return
	end

	if rank:match('[+-]$') then
		if rank:sub(-1) == "-" then
			step = 1
			to = #self.ranks
		else
			step = -1
			to = 1
		end

		rank = rank:sub(1, -2)
	end

	if rank == p.flags.preferred then
		rankPos = 1
	elseif rank == p.flags.normal then
		rankPos = 2
	elseif rank == 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 to then
		for i = rankPos, to, step do
			self.ranks[i] = true
		end
	else
		self.ranks[rankPos] = 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.periods[periodPos] = true
end

function Config:qualifierMatches(claim, id, value)
	local qualifiers

	if claim.qualifiers then qualifiers = claim.qualifiers[id] end
	if qualifiers then
		for _, qualifier in pairs(qualifiers) do
			if self:snakEqualsValue(qualifier, 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.ranks[rankPos] and self.foundRank >= rankPos)
	else
		return self.ranks[rankPos]
	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.periods[1] and self.periods[2] and self.periods[3] then
		-- any time
		return true
	end

	startTime = self:getSingleRawQualifier(claim, aliasesP.startTime)
	endTime = self:getSingleRawQualifier(claim, aliasesP.endTime)

	if startTime and endTime and datePrecedesDate(endTime, startTime) then
		-- invalidate end time if it precedes start time
		endTime = nil
	end

	if self.periods[1] then
		-- future
		if startTime and datePrecedesDate(self.atDate, startTime) then
			return true
		end
	end

	if self.periods[2] then
		-- current
		if (not startTime or not datePrecedesDate(self.atDate, startTime)) and
		   (not endTime or datePrecedesDate(self.atDate, endTime)) then
			return true
		end
	end

	if self.periods[3] then
		-- former
		if endTime and not datePrecedesDate(self.atDate, endTime) 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.statesByParam[parameters.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.freeUnit = true
		return true
	elseif flag == p.flags.number then
		self.curState.freeNumber = 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
		self.filterBeforeRank = 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:processCommand(command, general)
	local param, level

	if not command then
		return false
	end

	-- prevent general commands from being processed as valid commands if we only expect claim commands
	if general then
		if command == p.generalCommands.alias or command == p.generalCommands.aliases then
			param = parameters.alias
			level = 2  -- level 1 hook will be treated as a level 2 hook
		elseif command == p.generalCommands.badge or command == p.generalCommands.badges then
			param = parameters.badge
			level = 2  -- level 1 hook will be treated as a level 2 hook
		else
			return false
		end
	elseif command == p.claimCommands.property or command == p.claimCommands.properties then
		param = parameters.property
		level = 1
	elseif command == p.claimCommands.qualifier or command == p.claimCommands.qualifiers then
		self.qualifiersCount = self.qualifiersCount + 1
		param = parameters.qualifier .. self.qualifiersCount
		self.separators["sep%"..param] = {defaultSeparators["sep%q0"]}
		level = 2
	elseif command == p.claimCommands.reference or command == p.claimCommands.references then
		param = parameters.reference
		level = 2
	else
		return nil
	end

	if self.statesByParam[param] then
		return false
	end

	-- create a new state for each command
	self.curState = State:new(self, level, param)

	if command == p.claimCommands.property or
	   command == p.claimCommands.qualifier or
	   command == p.claimCommands.reference or
	   command == p.generalCommands.alias or
	   command == p.generalCommands.badge then
		self.curState.maxResults = 1
	end

	return true
end

function Config:processCommandOrFlag(commandOrFlag)
	local success = self:processCommand(commandOrFlag)

	if success == nil then
		success = self:processFlag(commandOrFlag)
	end

	return success
end

function Config:processSeparators(args)
	for i, v in pairs(self.separators) do
		if args[i] then
			self.separators[i][1] = replaceSpecialChars(args[i])
		end
	end
end

function State:isSourced(claim)
	return self.hooksByParam[parameters.reference](self, claim)
end

function State:claimMatches(claim)
	local matches

	-- 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
	matches = (matches and self.conf:rankMatches((rankTable[claim.rank] or {})[1]) 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))
	end

	return matches
end

function State:newSortFunction()
	local sortPaths = self.sortPaths
	local sortable = self.sortable
	local none = {""}

	local function resolveValues(sortPath, a, b)
		local aVal = nil
		local bVal = nil
		local sortKey = nil

		for _, subPath in ipairs(sortPath) do
			local aSub, bSub, key

			if #subPath == 0 then
				aSub = a
				bSub = b
			else
				if #subPath == 1 then
					aSub = subPath[1]
					bSub = subPath[1]

					if subPath.key then
						key = subPath[1]
					end
				else
					aSub, bSub, key = resolveValues(subPath, a, b)
				end

				sortKey = sortKey or key
			end

			if not aVal then
				aVal = aSub
				bVal = bSub
			else
				aVal = aVal[aSub]
				bVal = bVal[bSub]
			end
		end

		return aVal, bVal, sortKey
	end

	return function(a, b)
		for _, sortPath in ipairs(sortPaths) do
			local valLen, aPart, bPart
			local aValue, bValue, sortKey = resolveValues(sortPath, a, b)

			if not sortKey or sortable[sortKey] then
				aValue = aValue or none
				bValue = bValue or none

				if aValue.label or bValue.label then
					aValue = {aValue.label or ""}
					bValue = {bValue.label or ""}
					valLen = 1
				else
					valLen = aValue.len or #aValue
				end

				for i = 1, valLen do
					aPart = aValue[i]
					bPart = bValue[i]

					if aPart ~= bPart then
						if aPart == nil then
							return not sortPath.desc
						elseif bPart == nil then
							return sortPath.desc
						elseif aPart == p.SOMEVALUE or aPart == "" then
							if aPart == p.SOMEVALUE and bPart == "" then
								return true
							end
							return false
						elseif bPart == p.SOMEVALUE or bPart == "" then
							if bPart == p.SOMEVALUE and aPart == "" then
								return false
							end
							return true
						end

						if sortPath.desc then
							return aPart > bPart
						else
							return aPart < bPart
						end
					end
				end
			end
		end

		return false	
	end
end

function State:getHookFunction(param)
	if param:len() > 1 then
		param = param:sub(1, 1).."0"
	end

	-- fall back to 1 for getAlias and getBadge
	return (State[hookNames[param][self.level]] or State[hookNames[param][1]])
end

function State:newValueHook(param, id)
	local hook, idOrParam
	local func = self:getHookFunction(param)

	if self.level > 1 then
		idOrParam = 1
	else
		idOrParam = id or param
	end

	hook = function(state, statement)
		local datatype

		if not state.resultsByStatement[statement] then
			state.resultsByStatement[statement] = {}
		end

		if not state.resultsByStatement[statement][idOrParam] then
			state.resultsByStatement[statement][idOrParam] = func(state, statement, idOrParam)

			if not state.resultsDatatype then
				state.resultsDatatype = copyTable(getmetatable(state.resultsByStatement[statement][1]).datatype)
			end
		end

		return (#state.resultsByStatement[statement][idOrParam] > 0)
	end

	self.hooksByParam[idOrParam] = hook

	return hook
end

function State:prepareSortKey(sortKey)
	local desc = false
	local sortPath = nil
	local param = nil
	local id = nil
	local newID = nil

	if sortKey:match('[+-]$') then
		if sortKey:sub(-1) == "-" then
			desc = true
		end

		sortKey = sortKey:sub(1, -2)
	end

	if sortKey == RANK then
		return {{rankTable}, {{}, {"rank"}}, desc=desc}
	elseif sortKey:sub(1,1) == '%' then
		-- param <= param

		sortKey = sortKey:sub(2)
		param = sortKey

		if param == parameters.property then
			sortPath = {{self.resultsByStatement}, {}, {param, key=true}, desc=desc}
		else
			if param == parameters.qualifier then
				param = parameters.qualifier.."1"
			elseif not param:match('^'..parameters.qualifier..'%d+$') then
				return nil
			end

			sortPath = {{self.resultsByStatement}, {}, {param, key=true}, {1}, desc=desc}
		end

		if not self.conf.statesByParam[param] then
			return nil
		end
	else
		local baseParam, level, state
		local index = 0

		if sortKey == PROP then
			id = sortKey
			baseParam = parameters.property
			level = 1
			sortPath = {{self.resultsByStatement}, {}, {id, key=true}, desc=desc}
		else
			sortKey = replaceAlias(sortKey):upper()
			id = sortKey

			if not isPropertyID(id) then
				return nil
			end

			baseParam = parameters.qualifier.."0"
			level = 2

			sortPath = {{self.resultsByStatement}, {}, {id, key=true}, {1}, desc=desc}
		end

		if not self.conf.statesByID[id] then
			self.conf.statesByID[id] = {}
		end

		repeat
			index = index + 1

			if self.conf.statesByID[id][index] then
				-- id <= param

				state = self.conf.statesByID[id][index]
				param = state.param
			else
				-- id <= id

				param = baseParam
				newID = id
				state = State:new(self.conf, level, param, newID)
				state.freeNumber = true
				state.maxResults = 1
				self.conf.statesByParam[newID] = state
			end
		until not state.rawValue and not (state.freeUnit and not state.freeNumber)

		if id == PROP and index > 1 then
			self.propState = state
			self.propState.resultsByStatement = self.resultsByStatement
		end
	end

	return sortPath, param, id, newID
end

function State:newValidationHook(param, id, newID)
	local invalid = false
	local validated = false

	local idOrParam = id or param
	local newIdOrParam = newID or param

	if not self.hooksByParam[newIdOrParam] then
		self:newValueHook(param, newID)
	end

	local hook = self.hooksByParam[newIdOrParam]

	local function validationHook(state, claim)
		if invalid then
			return false
		end

		if hook(state.propState or state, claim) and not validated then
			local datatype

			validated = true
			datatype = getmetatable(state.resultsByStatement[claim][newIdOrParam]).datatype[1]

			if datatype == UNKNOWN then
				invalid = true
				return false
			end

			state.sortable[idOrParam] = true
		end

		state.resultsByStatement[claim][idOrParam] = state.resultsByStatement[claim][newIdOrParam]

		return true
	end

	self.valHooksByIdOrParam[idOrParam] = validationHook
	self.valHooks[#self.valHooks + 1] = validationHook

	return validationHook
end

function State:parseFormat(formatStr)
	local parsedFormat, hooks = parseFormat(self, formatStr)

	-- make sure that at least one required parameter has been defined
	if not next(parsedFormat.req) then
		throwError("missing-required-parameter")
	end

	-- make sure that the separator parameter "%s" is not amongst the required parameters
	if parsedFormat.req[parameters.separator] then
		throwError("extra-required-parameter", "%"..parameters.separator)
	end

	self.metaTable = {
		format = parsedFormat,
		datatype = {CLAIM},
		__tostring = toString
	}

	return hooks
end

-- level 1 hook
function State:getProperty(claim)
	return self:getValue(claim.mainsnak)
end

-- level 1 hook
function State:getQualifiers(claim, param)
	local qualifiers

	if claim.qualifiers then qualifiers = claim.qualifiers[self.conf.qualifierIDs[param] or param] end
	if qualifiers then
		-- iterate through claim's qualifier statements to collect their values
		self.conf.statesByParam[param]:iterate(qualifiers)  -- pass qualifier state
	end

	-- return array with multiple value objects (or empty array if there were no results)
	return self.conf.statesByParam[param]:getAndResetResults()
end

-- level 2 hook
function State:getQualifier(snak)
	return self:getValue(snak)
end

-- level 1 hook
function State:getAllQualifiers(claim, param)
	local param0
	local array = setmetatable({}, {sep=self.conf.separators["sep%"..param], __tostring=toString})

	-- iterate through the results of the separate "qualifier(s)" commands
	for i = 1, self.conf.qualifiersCount do
		param0 = param..i

		-- add the result if there is any, calling the hook in the process if it's not been called yet
		if self.hooksByParam[param0](self, claim) then
			array[#array + 1] = self.resultsByStatement[claim][param0]
		end
	end

	return array
end

-- level 1 hook
function State:getReferences(claim, param)
	if claim.references then
		-- iterate through claim's reference statements to collect their values
		self.conf.statesByParam[param]:iterate(claim.references)  -- pass reference state
	end

	-- return array with multiple value objects (or empty array if there were no results)
	return self.conf.statesByParam[param]:getAndResetResults()
end

-- level 2 hook
function State:getReference(statement)
	local key, keyNum, citeWeb, citeQ, label, mt2
	local mt = {datatype={REFERENCE}, __tostring=toString, __pairs=npairs}
	local value = setmetatable({}, mt)
	local params = {}
	local paramKeys = {}
	local skipKeys = {}
	local citeValues = {['web'] = {}, ['q'] = {}}
	local citeValueKeys = {['web'] = {}, ['q'] = {}}
	local citeMismatch = {}
	local useCite = nil
	local useValues = {}
	local useValueKeys = nil
	local str = nil

	local version = 2  -- increment this each time the below logic is changed to avoid conflict errors

	if not statement.snaks then
		return value
	end

	-- if we've parsed the exact same reference before, then return the cached one
	-- (note that this means that multiple occurences of the same value object could end up in the results)
	if self.references[statement.hash] then
		return self.references[statement.hash]
	end

	self.references[statement.hash] = value

	-- don't include "imported from", which is added by a bot
	if statement.snaks[aliasesP.importedFrom] then
		statement.snaks[aliasesP.importedFrom] = nil
	end

	-- don't include "inferred from", which is added by a bot
	if statement.snaks[aliasesP.inferredFrom] then
		statement.snaks[aliasesP.inferredFrom] = nil
	end

	-- don't include "type of reference"
	if statement.snaks[aliasesP.typeOfReference] then
		statement.snaks[aliasesP.typeOfReference] = nil
	end

	-- don't include "image" to prevent littering
	if statement.snaks[aliasesP.image] then
		statement.snaks[aliasesP.image] = nil
	end

	-- don't include "language" if it is equal to the local one
	if tostring(self:getReferenceDetail(statement.snaks[aliasesP.language])[1]) == self.conf.langName then
		statement.snaks[aliasesP.language] = nil
	end

	-- retrieve all the other parameters
	for i in pairs(statement.snaks) do

		-- multiple authors may be given
		if i == aliasesP.author then
			params[i] = self:getReferenceDetails(statement.snaks[i], false, self.linked, true, " & ")  -- link = true/false, anyLang = true
		else
			params[i] = self:getReferenceDetail(statement.snaks[i], false, (self.linked or (i == aliasesP.statedIn)) and (statement.snaks[i][1].datatype ~= 'url'), true)  -- link = true/false, anyLang = true
		end

		if not params[i][1] then
			params[i] = nil
		else
			paramKeys[#paramKeys + 1] = i

			-- add the parameter to each matching type of citation
			for j in pairs(citeValues) do
				label = ""

				-- do so if there was no mismatch with a previous parameter
				if not citeMismatch[j] then
					if j == 'q' and statement.snaks[i][1].datatype == 'external-id' then
						key = 'external-id'
						label = tostring(self.conf:getLabel(i))
					else
						key = i
					end

					-- check if this parameter is not mismatching itself
					if i18n['cite'][j][key] then
						key = i18n['cite'][j][key]

						-- continue if an option is available in the corresponding cite template
						if key ~= "" then
							local num = ""
							local k = 1

							while k <= #params[i] do
								keyNum = key..num

								citeValues[j][keyNum] = setmetatable({}, {sep={""}, __tostring=toString})  -- "sep" is needed to make this a recognizable array, even though it will not be used
								citeValueKeys[j][#citeValueKeys[j] + 1] = keyNum

								-- add the external ID's label to the format if we have one
								if label ~= "" then
									citeValues[j][keyNum][1] = copyValue(params[i][k])
									mt2 = getmetatable(citeValues[j][keyNum][1])
									mt2.format = mergeArrays({label, " "}, mt2.format or {tostring(citeValues[j][keyNum][1])})
								else
									citeValues[j][keyNum][1] = params[i][k]
								end

								k = k + 1
								num = k
							end
						end
					else
						citeMismatch[j] = true
					end
				end
			end
		end
	end

	-- get title of general template for citing web references
	citeWeb = ({split(mw.wikibase.getSitelink(aliasesQ.citeWeb) or "", ":")})[2]  -- split off namespace from front

	-- get title of template that expands stated-in references into citations
	citeQ = ({split(mw.wikibase.getSitelink(aliasesQ.citeQ) or "", ":")})[2]  -- split off namespace from front

	-- (1) use the general template for citing web references if there is a match and if at least both "reference URL" and "title" are present
	if citeWeb and not citeMismatch['web'] and citeValues['web'][i18n['cite']['web'][aliasesP.referenceURL]] and citeValues['web'][i18n['cite']['web'][aliasesP.title]] then
		useCite = citeWeb
		useValues = citeValues['web']
		useValueKeys = citeValueKeys['web']

	-- (2) use the template that expands stated-in references into citations if there is a match and if at least "stated in" is present
	elseif citeQ and not citeMismatch['q'] and citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]] then

		-- we need the raw "stated in" Q-identifier for the this template
		citeValues['q'][i18n['cite']['q'][aliasesP.statedIn]][1] = self:getReferenceDetail(statement.snaks[aliasesP.statedIn], true)[1]  -- raw = true

		useCite = citeQ
		useValues = citeValues['q']
		useValueKeys = citeValueKeys['q']
	end

	if useCite then

		-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors
		table.sort(useValueKeys)

		-- if this module is being substituted then build a regular template call, otherwise expand the template
		if mw.isSubsting() then
			mt.format = {"{{", useCite, params={}, req={}}

			-- iterate through the sorted keys
			for _, key in ipairs(useValueKeys) do
				mt2 = getmetatable(useValues[key][1])
				mt2.sub = {["|"] = ENC_PIPE}

				value[key] = useValues[key]

				mt.format[#mt.format + 1] = "|"
				mt.format[#mt.format + 1] = key
				mt.format[#mt.format + 1] = "="
				mt.format[#mt.format + 1] = key
				mt.format.params[#mt.format] = true
				mt.format.req[key] = true
			end

			mt.format[#mt.format + 1] = "}}"
		else
			for _, key in ipairs(useValueKeys) do
				value[key] = useValues[key]
			end

			mt.expand = useCite
		end

	-- (3) else, do some default rendering of name-value pairs, but only if at least "stated in", "reference URL" or "title" is present
	elseif params[aliasesP.statedIn] or params[aliasesP.referenceURL] or params[aliasesP.title] then
		mt.format = {params={}, req={}}

		-- start by adding authors up front
		if params[aliasesP.author] then
			label = tostring(self.conf:getLabel(aliasesP.author))

			if label == "" then
				label = aliasesP.author
			end

			value[label] = params[aliasesP.author]

			mt.format[1] = label
			mt.format.params[1] = true
			mt.format.req[label] = true
			mt.format[2] = "; "
		end

		-- then add "reference URL" and "title", combining them into one link if both are present
		if params[aliasesP.referenceURL] then
			label = tostring(self.conf:getLabel(aliasesP.referenceURL))

			if label == "" then
				label = aliasesP.referenceURL
			end

			value[label] = params[aliasesP.referenceURL]

			mt.format[#mt.format + 1] = '['
			mt.format[#mt.format + 1] = label
			mt.format.params[#mt.format] = true
			mt.format.req[label] = true
			mt.format[#mt.format + 1] = ' '

			if not params[aliasesP.title] then
				mt.format[#mt.format + 1] = label
				mt.format.params[#mt.format] = true
				mt.format.req[label] = true
				mt.format[#mt.format + 1] = ']'
			else
				str = ']'
			end
		end

		if params[aliasesP.title] then
			label = tostring(self.conf:getLabel(aliasesP.title))

			if label == "" then
				label = aliasesP.title
			end

			value[label] = params[aliasesP.title]

			mt.format[#mt.format + 1] = '"'
			mt.format[#mt.format + 1] = label
			mt.format.params[#mt.format] = true
			mt.format.req[label] = true
			mt.format[#mt.format + 1] = '"'
			mt.format[#mt.format + 1] = str
		end

		-- then add "stated in"
		if params[aliasesP.statedIn] then
			label = tostring(self.conf:getLabel(aliasesP.statedIn))

			if label == "" then
				label = aliasesP.statedIn
			end

			value[label] = params[aliasesP.statedIn]

			mt.format[#mt.format + 1] = "; "
			mt.format[#mt.format + 1] = "''"
			mt.format[#mt.format + 1] = label
			mt.format.params[#mt.format] = true
			mt.format.req[label] = true
			mt.format[#mt.format + 1] = "''"
		end

		-- mark previously added parameters so that they won't be added a second time
		skipKeys[aliasesP.author] = true
		skipKeys[aliasesP.referenceURL] = true
		skipKeys[aliasesP.title] = true
		skipKeys[aliasesP.statedIn] = true

		-- make sure that the parameters are added in the exact same order all the time to avoid conflict errors
		table.sort(paramKeys)

		-- add the rest of the parameters
		for _, key in ipairs(paramKeys) do
			if not skipKeys[key] then
				label = tostring(self.conf:getLabel(key))

				if label ~= "" then
					value[label] = params[key]

					mt.format[#mt.format + 1] = "; "
					mt.format[#mt.format + 1] = label
					mt.format[#mt.format + 1] = ": "
					mt.format[#mt.format + 1] = label
					mt.format.params[#mt.format] = true
					mt.format.req[label] = true
				end
			end
		end

		mt.format[#mt.format + 1] = "."
	end

	if not next(params) or not next(value) then
		return value  -- empty value
	end

	value[1] = params
	mt.hash = statement.hash

	if not self.rawValue then
		local curTime = ""

		-- if this module is being substituted then add a timestamp to the hash to avoid future conflict errors,
		-- which could occur when labels on Wikidata have been changed in the meantime while the substitution remains static
		if mw.isSubsting() then
			curTime = "-" .. self.conf.curTime
		end

		-- this should become a <ref> tag, so save the reference's hash for later
		mt.tag = {"ref", {name = "wikidata-" .. statement.hash .. curTime .. "-v" .. (tonumber(i18n['cite']['version']) + version)}}
	end

	return value
end

-- gets a detail of one particular type for a reference
function State:getReferenceDetail(snaks, raw, link, anyLang)
	local value
	local switchLang = anyLang or false
	local array = setmetatable({}, {sep={""}, __tostring=toString})  -- "sep" is needed to make this a recognizable array, even though it will not be used

	if not snaks then
		return array
	end

	-- if anyLang, first try the local language and otherwise any language
	repeat
		for _, snak in ipairs(snaks) do
			value = self.conf:getValue(snak, raw, link, false, anyLang and not switchLang, false, false, true)  -- noSpecial = true

			if value[1] then
				array[1] = value
				return array
			end
		end

		if not anyLang then
			break
		end

		switchLang = not switchLang
	until anyLang and switchLang

	return array
end

-- gets the details of one particular type for a reference
function State:getReferenceDetails(snaks, raw, link, anyLang, sep)
	local value
	local array = setmetatable({}, {sep={sep or ""}, __tostring=toString})

	if not snaks then
		return array
	end

	for _, snak in ipairs(snaks) do
		value = self.conf:getValue(snak, raw, link, false, anyLang, false, false, true)  -- noSpecial = true

		if value[1] then
			array[#array + 1] = value
		end
	end

	return array
end

-- level 1 hook
function State:getAlias(object)
	local alias = object.value
	local title = nil

	if alias 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
			return ({buildWikilink(title, alias)})[1]
		end
	end

	return setmetatable({alias}, {__tostring=toString})
end

-- level 1 hook
function State:getBadge(value)
	return ({self.conf:getLabel(value, self.rawValue, self.linked, self.shortName)})[1]
end

-- level 1 hook
function State:getSeparator()
	return self.conf.movSeparator
end

function State:addToResults(statement)
	self.results[#self.results + 1] = self.resultsByStatement[statement][1]

	if #self.results == self.maxResults then
		return nil
	end

	return true
end

function State:getAndResetResults()
	local results = setmetatable(self.results, {sep=self.separator, datatype=self.resultsDatatype, __tostring=toString})

	-- reset results before iterating over next dataset
	self.results = {}
	self.resultsByStatement = {}
	self.resultsDatatype = nil

	if self.level == 1 and results[1] and results[#results][parameters.separator] then
		results[#results][parameters.separator] = self.conf.puncMark
	end

	return results
end

-- this function may return nil, in which case the iterate function will break its loop
function State:callHooks(hooks, statement)
	local lastResult = nil
	local i = 1

	-- loop through the hooks in order and stop if one gives a negative result
	while hooks[i] do
		lastResult = hooks[i](self, statement)

		-- check if false or nil
		if not lastResult then
			return lastResult
		end

		i = i + 1
	end

	return lastResult
end

--cycle:
--	iterate(statements, hooks):
--		for statement in statements:
--			valueHook:					state.resultsByStatement[statement][param or 1] = func(state, statement, param)
--										func: {if lvl 2 hook}{cycle}
--			{if param}{persistHook:		state.resultsByStatement[statement][1][param] = state.resultsByStatement[statement][param]}
--			addToResults(statement):	state.results[#state.results + 1] = state.resultsByStatement[statement][1]
--		:rof
--	getAndResetResults:			return state.results {finally}{
--									state.results = {}
--									state.resultsByStatement = {}
--								}
--:elcyc
function State:iterate(statements, hooks)
	hooks = hooks or self.hooks

	for _, statement in ipairs(statements) do

		-- call hooks and break if the returned result is nil, which typically happens
		-- when addToResults found that we collected the maximum number of results
		if (self:callHooks(hooks, statement) == nil) then
			break
		end
	end
end

function State:iterateHooks(claims, hooks)
	local i = 1
	hooks = hooks or self.hooks

	while hooks[i] do
		local retry = false

		for _, claim in ipairs(claims) do
			local result = hooks[i](self, claim)

			if not result then
				if result == nil then
					retry = true
				end

				break
			end
		end

		if not retry then
			i = i + 1
		end
	end
end

--==-- Public functions --==--

local function claimCommand(args, funcName)
	local lastArg, hooks, claims, sortKey, sortKeys
	local sortHooks = {}
	local value = setmetatable({}, {__tostring=toString})
	local cfg = Config:new()

	cfg:processCommand(funcName)  -- process first command (== function name)

	-- set the date if given;
	-- must come BEFORE processing the flags
	if args[p.args.date] then
		cfg.atDate = {parseDate(args[p.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:processCommandOrFlag(lastArg)

	cfg.filterBeforeRank = cfg.filterBeforeRank or not (cfg.periods[1] and cfg.periods[2] and cfg.periods[3])

	-- get the entity ID from either the positional argument, the eid argument or the page argument
	cfg.entityID, cfg.propertyID = getEntityId(lastArg, args[p.args.eid], args[p.args.page])

	if cfg.entityID == "" then
		return value  -- empty; we cannot continue without a valid entity ID
	end

	if not cfg.propertyID then
		cfg.propertyID = nextArg(args)
	end

	cfg.propertyID = replaceAlias(cfg.propertyID)

	if not cfg.propertyID then
		return value  -- empty; we cannot continue without a property ID
	end

	cfg.propertyID = cfg.propertyID:upper()

	if cfg.statesByParam[parameters.qualifier.."1"] then
		-- do further processing if a "qualifier(s)" command was given

		if #args - args.pointer + 1 > cfg.qualifiersCount then
			-- claim ID or literal value has been given

			cfg.propertyValue = nextArg(args)
			cfg.filterBeforeRank = true
		end

		-- for each given qualifier ID, check if it is an alias and add it
		for i = 1, cfg.qualifiersCount do
			local param
			local qualifierID = nextArg(args)

			if not qualifierID then
				break
			end

			param = parameters.qualifier..i
			qualifierID = replaceAlias(qualifierID):upper()

			cfg.qualifierIDs[param] = qualifierID
			cfg:addToStatesByID(cfg.statesByParam[param], qualifierID)
		end
	elseif cfg.statesByParam[parameters.reference] then
		-- do further processing if "reference(s)" command was given

		cfg.propertyValue = nextArg(args)
		cfg.filterBeforeRank = true
	end

	-- process qualifier matching values, analogous to cfg.propertyValue
	for i, v in npairs(args) do
		local id = replaceAlias(i):upper()

		if isPropertyID(id) then
			cfg.qualifierIDsAndValues[id] = v
			cfg.filterBeforeRank = true
		end
	end

	-- potential optimization if only 'preferred' ranked claims are desired,
	-- or if the 'best' flag was given while no other filter flags were given
	if not (cfg.ranks[2] or cfg.ranks[3]) or (cfg.bestRank and not cfg.filterBeforeRank) then

		-- returns either only 'preferred' ranked claims or only 'normal' ranked claims
		claims = mw.wikibase.getBestStatements(cfg.entityID, cfg.propertyID)

		if #claims == 0 then
			-- no claims with rank 'preferred' or 'normal' found,
			-- property might only contain claims with rank 'deprecated'

			if not cfg.ranks[3] then
				return value  -- empty; we don't want 'deprecated' claims, so we're done
			end

			claims = nil  -- get all statements instead
		elseif not cfg.ranks[rankTable[claims[1].rank][1]] then
			-- the best ranked claims don't have the desired rank

			-- if the best ranked claims have rank 'normal' which isn't desired,
			-- then the property might only contain claims with rank 'deprecated'
			if claims[1].rank == "normal" and not cfg.ranks[3] then
				return value  -- empty; we don't want 'deprecated' claims, so we're done
			end

			claims = nil  -- get all statements instead
		end
	end

	if not claims then
		claims = mw.wikibase.getAllStatements(cfg.entityID, cfg.propertyID)
	end

	if #claims == 0 then
		return value  -- empty; there is no use to continue without any claims
	end

	-- create a state for "properties" if it doesn't exist yet, which will be used as a base configuration for each claim iteration
	if not cfg.statesByParam[parameters.property] then
		cfg.curState = State:new(cfg, 1, parameters.property, PROP)

		-- decrease potential overhead (in case this state will be used for sorting/matching)
		cfg.curState.freeNumber = true

		-- if the "single" flag has been given then this state should be equivalent to "property" (singular)
		if cfg.singleClaim then
			cfg.curState.maxResults = 1
		end
	else
		cfg.curState = cfg.statesByParam[parameters.property]
		cfg:addToStatesByID(cfg.curState, PROP)
	end

	-- parse the desired format, or choose an appropriate format
	if args["format"] then
		hooks = cfg.curState:parseFormat(args["format"])
	elseif cfg.statesByParam[parameters.qualifier.."1"] then  -- "qualifier(s)" command given
		if cfg.statesByParam[parameters.property] then  -- "propert(y|ies)" command given
			hooks = cfg.curState:parseFormat(formats.propertyWithQualifier)
		else
			hooks = cfg.curState:parseFormat(formats.qualifier)
		end
	elseif cfg.statesByParam[parameters.property] then  -- "propert(y|ies)" command given
		hooks = cfg.curState:parseFormat(formats.property)
	else  -- "reference(s)" command given
		hooks = cfg.curState:parseFormat(formats.reference)
	end

	hooks[#hooks + 1] = State.addToResults

	-- if a "qualifier(s)" command and no "propert(y|ies)" command has been given, make the movable separator a semicolon
	if cfg.statesByParam[parameters.qualifier.."1"] and not cfg.statesByParam[parameters.property] then
		cfg.separators["sep%s"][1] = ";"
	end

	-- if only "reference(s)" has been given, set the default separator to none (except when raw)
	if cfg.statesByParam[parameters.reference] and not cfg.statesByParam[parameters.property] and not cfg.statesByParam[parameters.qualifier.."1"]
	   and not cfg.statesByParam[parameters.reference].rawValue then
		cfg.separators["sep"][1] = ""
	end

	-- if exactly one "qualifier(s)" command has been given, make "sep%q" point to "sep%q1" to make them equivalent
	if cfg.qualifiersCount == 1 then
		cfg.separators["sep%q"] = cfg.separators["sep%q1"]
	end

	-- process overridden separator values;
	-- must come AFTER tweaking the default separators
	cfg:processSeparators(args)

	-- 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 processing the commands and parsing the format
	if cfg.sourcedOnly and not cfg.curState.hooksByParam[parameters.reference] then
		if not cfg.statesByParam[parameters.reference] then
			local refState = State:new(cfg, 2, parameters.reference)
			refState.maxResults = 1  -- decrease overhead
		end

		cfg.curState:newValueHook(parameters.reference)
	end

	table.insert(hooks, 1, State.claimMatches)

	-- if the best ranked claims are desired, we'll sort by rank first
	if cfg.bestRank then
		cfg.curState.sortPaths[1] = cfg.curState:prepareSortKey(RANK)
	end

	if args[p.args.sort] then
		sortKeys = args[p.args.sort]
	else
		sortKeys = RANK  -- by default, sort by rank
	end

	repeat
		local sortPath, param, id, newID

		sortKey, sortKeys = split(sortKeys, ",")
		sortKey = mw.text.trim(sortKey)

		-- additional sorting by rank is pointless if only the best rank is desired
		if not (cfg.bestRank and sortKey:match('^'..RANK..'[+-]?$')) then
			sortPath, param, id, newID = cfg.curState:prepareSortKey(sortKey)

			if sortPath then
				cfg.curState.sortPaths[#cfg.curState.sortPaths + 1] = sortPath

				if param and not cfg.curState.valHooksByIdOrParam[id or param] then
					sortHooks[#sortHooks + 1] = newOptionalHook{cfg.curState:newValidationHook(param, id, newID)}
				end
			end
		end
	until not sortKeys

	cfg.curState:iterate(claims, sortHooks)

	table.sort(claims, cfg.curState:newSortFunction())

	-- then iterate through the claims to collect values
	cfg.curState:iterate(claims, hooks)  -- pass property state with level 1 hooks

	value = cfg.curState:getAndResetResults()

	-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
	if cfg.editable and value[1] then
		local mt = getmetatable(value)
		mt.trail = cfg:getEditIcon()
	end

	return value
end

local function generalCommand(args, funcName)
	local lastArg
	local value = setmetatable({}, {__tostring=toString})
	local cfg = Config:new()

	-- process command (== function name); if false, then it's not "alias(es)" or "badge(s)"
	if not cfg:processCommand(funcName, true) then
		cfg.curState = State:new(cfg)
	end

	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, args[p.args.eid], args[p.args.page], true)

	if cfg.entityID == "" or not mw.wikibase.entityExists(cfg.entityID) then
		return value  -- empty; 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[1] = mw.wikibase.getSitelink(cfg.entityID)
		end

		if cfg.curState.linked and value[1] then
			value = buildWikilink(value[1])
		end
	elseif funcName == p.generalCommands.description then
		value[1] = mw.wikibase.getDescription(cfg.entityID)
	else
		local values

		cfg.entity = mw.wikibase.getEntity(cfg.entityID)

		if funcName == p.generalCommands.alias or funcName == p.generalCommands.aliases then
			if not cfg.entity.aliases or not cfg.entity.aliases[cfg.langCode] then
				return value  -- empty; there is no use to continue without any aliasses
			end

			values = cfg.entity.aliases[cfg.langCode]
		elseif funcName == p.generalCommands.badge or funcName == p.generalCommands.badges then
			if not cfg.entity.sitelinks or not cfg.entity.sitelinks[cfg.siteID] or not cfg.entity.sitelinks[cfg.siteID].badges then
				return value  -- empty; there is no use to continue without any badges
			end

			cfg.inSitelinks = true
			values = cfg.entity.sitelinks[cfg.siteID].badges
		end

		cfg.separators["sep"][1] = ", "

		-- process overridden separator values;
		-- must come AFTER tweaking the default separator
		cfg:processSeparators(args)

		-- iterate to collect values
		cfg.curState:iterate(values)

		value = cfg.curState:getAndResetResults()
	end

	-- if desired, add a clickable icon that may be used to edit the returned values on Wikidata
	if cfg.editable and value[1] then
		local mt = getmetatable(value)
		mt.trail = cfg:getEditIcon()
	end

	return value
end

-- modules that include this module may 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 stringWrapper(frameOrArgs)
			local frame, args

			-- check if Wikidata is available to prevent errors
			if not mw.wikibase then
				return ""
			end

			-- assumption: a frame always has an args table
			if frameOrArgs.args then
				-- called by wikitext
				frame = frameOrArgs
				args = copyTable(frame.args)
			else
				-- called by module
				args = frameOrArgs
			end

			args.pointer = 1

			loadI18n(aliasesP, frame)

			return tostring(commandFunc(args, commandName))
		end

		p[commandName] = stringWrapper

		local function tableWrapper(args)

			-- check if Wikidata is available to prevent errors
			if not mw.wikibase then
				return nil
			end

			args = copyTable(args)
			args.pointer = 1

			loadI18n(aliasesP)

			return commandFunc(args, commandName)
		end

		p["_" .. commandName] = tableWrapper
	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)
	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.args[1] then
		throwError("no-function-specified")
	end

	f = mw.text.trim(frame.args[1])

	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