A documentação para este módulo pode ser criada na página Módulo:Wd/doc

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

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",
    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 = {
    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[ <span style=\"font-size:85\\%\">(%q)</span>][%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 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

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 buildWikilink(target, label)
    if not label or target == label then
        return "[[" .. target .. "]]"
    else
        return "[[" .. target .. "|" .. label .. "]]"
    end
end

-- used to make frame.args mutable, to replace #frame.args (which is always 0)
-- with the actual amount and to simply copy tables
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

-- 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

local function split(str, del)
    local out = {}
    local i, j = str:find(del)

    if i and j then
        out[1] = str:sub(1, i - 1)
        out[2] = str:sub(j + 1)
    else
        out[1] = str
    end

    return out
end

local function parseWikidataURL(url)
    local id

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

        if id[2] then
            return "Q" .. id[2]
        end
    end

    return nil
end

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(aY, aM, aD, bY, bM, bD)
    if aY == nil or bY == nil then
        return nil
    end
    aM = aM or 1
    aD = aD or 1
    bM = bM or 1
    bD = bD or 1

    if aY < bY then
        return true
    end

    if aY > bY then
        return false
    end

    if aM < bM then
        return true
    end

    if aM > bM then
        return false
    end

    if aD < bD then
        return true
    end

    return false
end

local function getHookName(param, index)
    if hookNames[param] then
        return hookNames[param][index]
    elseif param:len() > 2 then
        return hookNames[param: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
                params[cur.str] = true
                cur.parent.req[cur.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.req[parameters.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 = rankTable[v.rank] or 4
        ranks[rankPos][#ranks[rankPos] + 1] = v
    end

    sorted = ranks[1]
    sorted = mergeArrays(sorted, ranks[2])
    sorted = mergeArrays(sorted, ranks[3])

    return sorted
end

-- if id == nil then item connected to current page is used
function Config:getLabel(id, raw, link, short)
    local label = nil
    local title = nil
    local prefix= ""

    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

            if id:sub(1,1) == "P" then
                prefix = "Property:"
            end
        end

        prefix = "d:" .. prefix

        title = 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)
        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
                title = id
                prefix = "d:Property:"
            end
        end

        if title then
            label = buildWikilink(prefix .. title, label)
        end
    end

    return label
end

function Config:getEditIcon()
    local value = ""
    local prefix = ""
    local front = "&nbsp;"
    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.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['info']['edit-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 valuesArray[i].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 valuesArray[j] and valuesArray[j].refHash do
                if valuesArray[i].refHash == valuesArray[j].refHash then
                    skip = true
                    break
                end
                j = j - 1
            end

            if not skip then
                -- add <ref> tag with the reference's hash as its name (to deduplicate references)
                outString = outString .. mw.getCurrentFrame():extensionTag("ref", valuesArray[i][1], {name = valuesArray[i].refHash})
            end
        else
            outString = outString .. valuesArray[i][1]
        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 < 0 then
                sign = -1
                y = y * sign
            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)

                        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
                            -- (e.g. 1901 for 20th century or 2001 for 3rd millennium)
                            yRound = (yRound - 1) * yFactor + 1
                        end
                    else
                        -- precision == 8
                        -- round decades down (e.g. 2010s)
                        yRound = math.floor(yDiv) * yFactor

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

                    if raw and 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)
                        yRound = yRound + yFactor - 1
                    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

                    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 = yRound * yFactor
                            if yRound == 1 then
                                suffix = i18n['datetime']['suffixes']['year']
                            else
                                suffix = i18n['datetime']['suffixes']['years']
                            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['datetime']['julian-calendar'], i18n['datetime']['julian'])..")"
                        else
                            calendar = " ("..i18n['datetime']['julian']..")"
                        end
                    else
                        calendar = "/"..i18n['datetime']['julian']
                    end
                end
            end

            if not raw then
                local ce = nil

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

                if ce then
                    if link then
                        ce = buildWikilink(i18n['datetime']['common-era'], ce)
                    end
                    suffix = suffix .. " " .. ce
                end

                value = tostring(yRound)

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

                    if d then
                        if self.mdyDate then
                            dateStr = dateStr .. " " .. d .. ","
                        else
                            dateStr = d .. " " .. dateStr
                        end
                    end

                    value = dateStr .. " " .. value
                end

                value = prefix .. value .. suffix .. calendar
            else
                value = padZeros(yRound * sign, 4)

                if m then
                    value = value .. "-" .. padZeros(m, 2)

                    if d then
                        value = value .. "-" .. padZeros(d, 2)
                    end
                end

                value = value .. calendar
            end

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

            local precision, unitsPerDegree, numDigits, strFormat, value, globe
            local latitude, latConv, latValue, latLink
            local longitude, lonConv, lonValue, lonLink
            local latDirection, latDirectionN, latDirectionS, latDirectionEN
            local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN
            local degSymbol, minSymbol, secSymbol, separator

            local latDegrees = nil
            local latMinutes = nil
            local latSeconds = nil
            local lonDegrees = nil
            local lonMinutes = nil
            local lonSeconds = nil

            local latDegSym = ""
            local latMinSym = ""
            local latSecSym = ""
            local lonDegSym = ""
            local lonMinSym = ""
            local lonSecSym = ""

            local latDirectionEN_N = "N"
            local latDirectionEN_S = "S"
            local lonDirectionEN_E = "E"
            local lonDirectionEN_W = "W"

            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 = latDirectionEN_N
                latDirectionS = latDirectionEN_S
                lonDirectionE = lonDirectionEN_E
                lonDirectionW = lonDirectionEN_W

                degSymbol = "/"
                minSymbol = "/"
                secSymbol = "/"
                separator = "/"
            end

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

            if latitude < 0 then
                latDirection = latDirectionS
                latDirectionEN = latDirectionEN_S
                latitude = math.abs(latitude)
            else
                latDirection = latDirectionN
                latDirectionEN = latDirectionEN_N
            end

            if longitude < 0 then
                lonDirection = lonDirectionW
                lonDirectionEN = lonDirectionEN_W
                longitude = math.abs(longitude)
            else
                lonDirection = lonDirectionE
                lonDirectionEN = lonDirectionEN_E
            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)

                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 = "[https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."&params="..latLink.."_"..latDirectionEN.."_"..lonLink.."_"..lonDirectionEN.."_globe:"..globe.." "..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 '<strong class="error">' .. errorText('unknown-data-type', subtype) .. '</strong>'
            end

            label = self:getLabel(itemID, raw, link, short)

            if label == "" then
                label = nil
            end

            return label
        else
            return '<strong class="error">' .. errorText('unknown-data-type', datatype) .. '</strong>'
        end
    elseif snak.snaktype == 'somevalue' and not noSpecial then
        if raw then
            return " "  -- single space represents 'somevalue'
        else
            return i18n['values']['unknown']
        end
    elseif snak.snaktype == 'novalue' and not noSpecial then
        if raw then
            return ""  -- empty string represents 'novalue'
        else
            return i18n['values']['none']
        end
    else
        return nil
    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

    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.ranks[i] = true
        end
    elseif rank:sub(-1) == "-" then
        for i = rankPos, #self.ranks 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 _, 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.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)
    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.periods[1] then
        -- future
        if startTimeY and datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], startTimeY, startTimeM, startTimeD) then
            return true
        end
    end

    if self.periods[2] then
        -- current
        if (startTimeY == nil or not datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], startTimeY, startTimeM, startTimeD)) and
           (endTimeY == nil or datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], endTimeY, endTimeM, endTimeD)) then
            return true
        end
    end

    if self.periods[3] then
        -- former
        if endTimeY and not datePrecedesDate(self.atDate[1], self.atDate[2], self.atDate[3], 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.states[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.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.states[param] then
        return false
    end

    -- create a new state for each command
    self.states[param] = State:new(self, param)

    -- use "%x" as the general parameter name
    self.states[param].parsedFormat = parseFormat(parameters.general)  -- will be overwritten for param=="%p"

    -- set the separator
    self.states[param].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.states[param].singleValue = true
    end

    self.curState = self.states[param]

    return true
end

function Config:processSeparators(args)
    local sep

    for i, v in pairs(self.separators) do
        if args[i] then
            sep = replaceSpecialChars(args[i])

            if sep ~= "" then
                self.separators[i][1] = {sep}
            else
                self.separators[i][1] = 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 = rankTable[claim.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 result[i] or not result[i][1] 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, result[v.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.results[i]

        -- if there is already some output, then add the separators
        if #out > 0 then
            sep = self.separator[1]  -- fixed separator
            result[parameters.separator] = {self.movSeparator[1]}  -- movable separator
        else
            sep = nil
            result[parameters.separator] = {self.puncMark[1]}  -- 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.qualifiers[self.conf.qualifierIDs[param]] end
    if qualifiers then
        -- iterate through claim's qualifier statements to collect their values;
        -- return array with multiple value objects
        return self.conf.states[param]:iterate(qualifiers, {[parameters.general] = hookNames[parameters.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.qualifier][1]  -- 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 result[parameters.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 result[parameters.qualifier..i] and result[parameters.qualifier..i][1] 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, result[parameters.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.states[parameters.reference]:iterate(claim.references, {[parameters.general] = hookNames[parameters.reference][2], 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 version = 1  -- 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.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 self:getReferenceDetail(statement.snaks, aliasesP.language) == self.conf.langName then
            statement.snaks[aliasesP.language] = nil
        end

        -- retrieve all the parameters
        for i in pairs(statement.snaks) do
            label = ""

            -- 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 #params[i] == 0 then
                params[i] = nil
            else
                if statement.snaks[i][1].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 citeMismatch[j] then
                        -- check if this parameter is not mismatching itself
                        if i18n['cite'][j][key] then
                            -- continue if an option is available in the corresponding cite template
                            if i18n['cite'][j][key] ~= "" then
                                citeParams[j][i18n['cite'][j][key]] = label .. params[i][1]

                                -- if there are multiple parameter values (authors), add those too
                                for k=2, #params[i] do
                                    citeParams[j][i18n['cite'][j][key]..k] = label .. params[i][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 citeParams['web'][i18n['cite']['web'][aliasesP.referenceURL]] and citeParams['web'][i18n['cite']['web'][aliasesP.title]] then
            useCite = citeWeb
            useParams = citeParams['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 citeParams['q'][i18n['cite']['q'][aliasesP.statedIn]] then
            -- we need the raw "stated in" Q-identifier for the this template
            citeParams['q'][i18n['cite']['q'][aliasesP.statedIn]] = self:getReferenceDetail(statement.snaks, aliasesP.statedIn, true)  -- raw = true

            useCite = citeQ
            useParams = citeParams['q']
        end

        if useCite and useParams then
            -- if this module is being substituted then build a regular template call, otherwise expand the template
            if mw.isSubsting() then
                for i, v in pairs(useParams) do
                    value = value .. "|" .. i .. "=" .. v
                end

                value = "{{" .. useCite .. value .. "}}"
            else
                value = mw.getCurrentFrame():expandTemplate{title=useCite, args=useParams}
            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
            citeParams['default'] = {}

            -- start by adding authors up front
            if params[aliasesP.author] and #params[aliasesP.author] > 0 then
                citeParams['default'][#citeParams['default'] + 1] = table.concat(params[aliasesP.author], " & ")
            end

            -- combine "reference URL" and "title" into one link if both are present
            if params[aliasesP.referenceURL] and params[aliasesP.title] then
                citeParams['default'][#citeParams['default'] + 1] = '[' .. params[aliasesP.referenceURL][1] .. ' "' .. params[aliasesP.title][1] .. '"]'
            elseif params[aliasesP.referenceURL] then
                citeParams['default'][#citeParams['default'] + 1] = params[aliasesP.referenceURL][1]
            elseif params[aliasesP.title] then
                citeParams['default'][#citeParams['default'] + 1] = '"' .. params[aliasesP.title][1] .. '"'
            end

            -- then add "stated in"
            if params[aliasesP.statedIn] then
                citeParams['default'][#citeParams['default'] + 1] = "''" .. params[aliasesP.statedIn][1] .. "''"
            end

            -- remove previously added parameters so that they won't be added a second time
            params[aliasesP.author] = nil
            params[aliasesP.referenceURL] = nil
            params[aliasesP.title] = nil
            params[aliasesP.statedIn] = nil

            -- add the rest of the parameters
            for i, v in pairs(params) do
                i = self.conf:getLabel(i)

                if i ~= "" then
                    citeParams['default'][#citeParams['default'] + 1] = i .. ": " .. v[1]
                end
            end

            value = table.concat(citeParams['default'], "; ")

            if value ~= "" then
                value = value .. "."
            end
        end

        if value ~= "" then
            value = {value}  -- create one value object

            if not self.rawValue then
                -- this should become a <ref> tag, so save the reference's hash for later
                value.refHash = "wikidata-" .. statement.hash .. "-v" .. (tonumber(i18n['cite']['version']) + 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 snaks[dType] then
        return nil
    end

    -- if anyLang, first try the local language and otherwise any language
    repeat
        for _, v in ipairs(snaks[dType]) 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 snaks[dType] then
        return {}
    end

    for _, v in ipairs(snaks[dType]) 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 result[param] and hooks[param] then
        valuesArray = self[hooks[param]](self, statement, param, result, hooks)  -- array with value objects

        -- add to the result
        if #valuesArray > 0 then
            result[param] = valuesArray
            result.count = result.count + 1
        else
            result[param] = {}  -- 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 = args[args.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 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:processFlagOrCommand(lastArg)

    -- 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 ""  -- 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.claims[cfg.propertyID] then
        return ""  -- there is no use to continue without any claims
    end

    claims = cfg.entity.claims[cfg.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.qualifierIDs[parameters.qualifier..i] = replaceAlias(nextArg(args) or ""):upper()
        end
    elseif cfg.states[parameters.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.states[parameters.property] then  -- "propert(y|ies)" command given
            parsedFormat, formatParams = parseFormat(formats.propertyWithQualifier)
        else
            parsedFormat, formatParams = parseFormat(formats.qualifier)
        end
    elseif cfg.states[parameters.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.states[parameters.property] then
        cfg.separators["sep"..parameters.separator][1] = {";"}
    end

    -- if only "reference(s)" has been given, set the default separator to none (except when raw)
    if cfg.states[parameters.reference] and not cfg.states[parameters.property] and cfg.states.qualifiersCount == 0
       and not cfg.states[parameters.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 formatParams[i] or formatParams[i:sub(1, 2)] then
            hooks[i] = 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 formatParams[parameters.qualifier] and cfg.states.qualifiersCount > 0 then
        hooks[parameters.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.states[parameters.property] then
        cfg.states[parameters.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.states[parameters.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.states[parameters.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.states[parameters.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 aliasesP[i] 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.qualifierIDsAndValues[replaceAlias(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.states[parameters.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, args[p.args.eid], args[p.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.aliases[cfg.langCode] then
                return ""  -- there is no use to continue without any aliasses
            end

            local aliases = cfg.entity.aliases[cfg.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 formatParams[parameters.alias] then
                hooks[parameters.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.sitelinks[cfg.siteID] or not cfg.entity.sitelinks[cfg.siteID].badges then
                return ""  -- there is no use to continue without any badges
            end

            local badges = cfg.entity.sitelinks[cfg.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 formatParams[parameters.badge] then
                hooks[parameters.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
        p[commandName] = 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)
    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