Jump to content

Module:Wd

ဝီကီးပီးဒီးယား က

Documentation for this module may be created at Module:Wd/doc

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

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

function loadSubmodules(frame)
  -- internationalization
  if not i18n then
    if frame then
      -- module invoked by page/template, get its title from frame
      i18n = require(frame:getTitle().."/i18n")
    else
      -- module included by other module, get its title from ...
      i18n = require(arg.."/i18n")
    end
  end
end

local aliasesP = {
  coord           = "P625",
  ---------------
  author          = "P50",
  publisher       = "P123",
  importedFrom    = "P143",
  statedIn        = "P248",
  publicationDate = "P577",
  startTime       = "P580",
  endTime         = "P582",
  retrieved       = "P813",
  referenceURL    = "P854",
  archiveURL      = "P1065",
  title           = "P1476",
  quote           = "P1683",
  shortName       = "P1813",
  language        = "P407",
  archiveDate     = "P2960"
}

local aliasesQ = {
  percentage              = "Q11229",
  prolepticJulianCalendar = "Q1985786"
}

local parameters = {
  property  = "%p",
  qualifier = "%q",
  reference = "%r",
  separator = "%s",
  general   = "%x"
}

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

local hookNames = {              -- {level_1, level_2}
  [parameters.property]         = {"getProperty"},
  [parameters.reference]        = {"getReferences", "getReference"},
  [parameters.qualifier]        = {"getAllQualifiers"},
  [parameters.qualifier.."\\d"] = {"getQualifiers", "getQualifier"}
}

-- 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 Config = {}
Config.__index = Config

-- allows for recursive calls
function Config.new()
  local cfg = {}
  setmetatable(cfg, Config)
  
  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.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.mdyDate = false
  cfg.singleClaim = false
  cfg.sourcedOnly = false
  
  cfg.pageTitle = false
  
  cfg.langCode = mw.language.getContentLanguage().code
  cfg.langName = mw.language.fetchLanguageName(cfg.langCode, cfg.langCode)
  cfg.langObj = mw.language.new(cfg.langCode)
  
  cfg.states = {}
  cfg.states.qualifiersCount = 0
  cfg.curState = nil
  
  cfg.prefetchedRefs = nil
  
  return cfg
end

local State = {}
State.__index = State

function State.new(cfg)
  local stt = {}
  setmetatable(stt, State)
  
  stt.conf = cfg
  
  stt.results = {}
  
  stt.parsedFormat = {}
  stt.separator = {}
  stt.movSeparator = {}
  stt.puncMark = {}
  
  stt.linked = false
  stt.rawValue = false
  stt.shortName = false
  stt.singleValue = false
  
  return stt
end

function replaceAlias(ID)
  if aliasesP[ID] then
    ID = aliasesP[ID]
  end
  
  return ID
end

function applyStringParams(str, ...)
  for i, v in ipairs(arg) do
    str = mw.ustring.gsub(str, "$"..i, v)
  end
  
  return str
end

function unknownDataTypeError(dataType)
  return applyStringParams(i18n['errors']['unknown-data-type'], dataType)
end

function missingRequiredParameterError()
  return i18n['errors']['missing-required-parameter']
end

function extraRequiredParameterError(param)
  return applyStringParams(i18n['errors']['extra-required-parameter'], param)
end

function getOrdinalSuffix(num)
  return i18n.getOrdinalSuffix(num)
end

function addDelimiters(num)
  return i18n.addDelimiters(num)
end

function replaceDecimalMark(num)
  return mw.ustring.gsub(num, "[.]", i18n['numeric']['decimal-mark'], 1)
end

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

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

function buildWikilink(target, label)
  if 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
function mergeArrays(a1, a2)
  for i = 1, #a2 do
    a1[#a1 + 1] = a2[i]
  end
  
  return a1
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 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 = "wikidata-" .. valuesArray[i].refHash})
      end
    else
      outString = outString .. valuesArray[i][1]
    end
  end

  return outString
end

function parseWikidataURL(url)
  local i, j
  
  if url:match('^http[s]?://') then
    i, j = url:find("Q")
    
    if i then
      return url:sub(i)
    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

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

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

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"
--   }
-- ]
--
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()
  
  return root, params
end

function convertRank(rank)
  if rank == "preferred" then
    return 1
  elseif rank == "normal" then
    return 2
  elseif rank == "deprecated" then
    return 3
  else
    return 4  -- default (in its literal sense)
  end
end

function sortOnRank(claims)
  local rankPos
  local ranks = {{}, {}, {}, {}}  -- preferred, normal, deprecated, (default)
  local sorted = {}
  
  for i, v in ipairs(claims) do
    rankPos = convertRank(v.rank)
    ranks[rankPos][#ranks[rankPos] + 1] = v
  end
  
  sorted = ranks[1]
  sorted = mergeArrays(sorted, ranks[2])
  sorted = mergeArrays(sorted, ranks[3])
  
  return sorted
end

function getShortName(ID)
  if ID then
    return p._property({ID, aliasesP.shortName})  -- "property" is single
  else
    return p._property({aliasesP.shortName})  -- "property" is single
  end
end

function getLabel(ID)
  if ID then
    return p._label({ID})
  else
    return p._label({})
  end
end

function Config:convertUnit(unit, link)
  link = link or false
  local itemID, label, lang, title
  
  if unit == "" or unit == "1" then
    return nil
  end
  
  itemID = parseWikidataURL(unit)
  
  if itemID then
    if itemID == aliasesQ.percentage then
      return "%"
    else
      label, lang = mw.wikibase.getLabelWithLang(itemID)
      
      -- don't allow language fallback
      if lang ~= self.langCode then
        label = nil
      end
      
      title = nil
      
      if link or label == nil then
        title = mw.wikibase.sitelink(itemID)
      end
      
      if link then
        if title then
          return " " .. buildWikilink(title, (label or title))
        end
        
        if not label then
          return " " .. buildWikilink("d:" .. itemID, itemID)
        end
      end
      
      return " " .. (label or title or itemID)
    end
  end
  
  return " " .. unit
end

function Config:getValue(snak, raw, link, short, anyLang)
  raw = raw or false
  link = link or false
  short = short or false
  anyLang = anyLang or false
  
  if snak.snaktype == 'value' then
    if snak.datavalue.type == 'string' then
      if snak.datatype == 'url' and link then
        -- create link explicitly
        if raw then
          -- will render as a linked number like [1]
          return "[" .. snak.datavalue.value .. "]"
        else
          return "[" .. snak.datavalue.value .. " " .. snak.datavalue.value .. "]"
        end
      elseif snak.datatype == 'commonsMedia' then
        if link then
          if raw then
            -- will render as a linked number like [1]
            return "[https://commons.wikimedia.org/wiki/File:" .. mw.ustring.gsub(snak.datavalue.value, " ", "_") .. "]"
          else
            return "[https://commons.wikimedia.org/wiki/File:" .. mw.ustring.gsub(snak.datavalue.value, " ", "_") .. " " .. snak.datavalue.value .. "]"
          end
        elseif not raw then
          return "[[File:" .. snak.datavalue.value .. "]]"
        else
          return snak.datavalue.value
        end
      elseif snak.datatype == 'geo-shape' and link then
        if raw then
          -- will render as a linked number like [1]
          return "[https://commons.wikimedia.org/wiki/" .. mw.ustring.gsub(snak.datavalue.value, " ", "_") .. "]"
        else
          return "[https://commons.wikimedia.org/wiki/" .. mw.ustring.gsub(snak.datavalue.value, " ", "_") .. " " .. snak.datavalue.value .. "]"
        end
      elseif snak.datatype == 'math' and not raw then
        return mw.getCurrentFrame():extensionTag("math", snak.datavalue.value)
      else
        return snak.datavalue.value
      end
    elseif snak.datavalue.type == 'monolingualtext' then
      if anyLang then
        return snak.datavalue.value['text'], snak.datavalue.value['language']
      elseif snak.datavalue.value['language'] == self.langCode then
        return snak.datavalue.value['text']
      else
        return nil
      end
    elseif snak.datavalue.type == 'quantity' then
      -- strip + signs from front
      local value = mw.ustring.gsub(snak.datavalue.value['amount'], "^\+(.+)$", "%1")
      
      if not raw then
        -- replace decimal mark based on locale
        value = replaceDecimalMark(value)
        
        -- add delimiters for readability
        value = addDelimiters(value)
        
        local unit = self:convertUnit(snak.datavalue.value['unit'], link)
        
        if unit then
          value = value .. unit
        end
      end
      
      return value
    elseif snak.datavalue.type == '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 = snak.datavalue.value['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(snak.datavalue.value['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 = 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(snak.datavalue.value['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 = tostring(yRound * sign)
        
        if m then
          value = value .. "-" .. m
          
          if d then
            value = value .. "-" .. d
          end
        end
        
        value = value .. calendar
      end
      
      return value
    elseif snak.datavalue.type == 'globecoordinate' then
      -- logic from https://github.com/DataValues/Geo
      
      local precision, numDigits, strFormat, value, globe
      local latValue, latitude, latDegrees, latMinutes, latSeconds
      local lonValue, longitude, lonDegrees, lonMinutes, lonSeconds
      local latDirection, latDirectionN, latDirectionS, latDirectionEN
      local lonDirection, lonDirectionE, lonDirectionW, lonDirectionEN
      
      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 = snak.datavalue.value['latitude']
      longitude = snak.datavalue.value['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 = snak.datavalue.value['precision']
      
      latitude = math.floor(latitude / precision + 0.5) * precision
      longitude = math.floor(longitude / precision + 0.5) * precision
      
      numDigits = math.ceil(-math.log10(3600 * precision))
      
      if numDigits < 0 or numDigits == -0 then
        numDigits = tonumber("0")  -- for some reason, 'numDigits = 0' may actually store '-0', so parse from string instead
      end
      
      strFormat = "%." .. numDigits .. "f"
      
      -- use string.format() to strip decimal point followed by a zero (.0) for whole numbers
      latSeconds = tonumber(strFormat:format(math.floor(latitude * 3600 * 10^numDigits + 0.5) / 10^numDigits))
      lonSeconds = tonumber(strFormat:format(math.floor(longitude * 3600 * 10^numDigits + 0.5) / 10^numDigits))
      
      latMinutes = math.floor(latSeconds / 60)
      lonMinutes = math.floor(lonSeconds / 60)
      
      latSeconds = latSeconds - (latMinutes * 60)
      lonSeconds = lonSeconds - (lonMinutes * 60)
      
      latDegrees = math.floor(latMinutes / 60)
      lonDegrees = math.floor(lonMinutes / 60)
      
      latMinutes = latMinutes - (latDegrees * 60)
      lonMinutes = lonMinutes - (lonDegrees * 60)
      
      latValue = latDegrees .. degSymbol
      lonValue = lonDegrees .. degSymbol
      
      if precision < 1 then
        latValue = latValue .. latMinutes .. minSymbol
        lonValue = lonValue .. lonMinutes .. minSymbol
      end
      
      if precision < (1 / 60) then
        latSeconds = strFormat:format(latSeconds)
        lonSeconds = strFormat:format(lonSeconds)
        
        if not raw then
          -- replace decimal marks based on locale
          latSeconds = replaceDecimalMark(latSeconds)
          lonSeconds = replaceDecimalMark(lonSeconds)
        end
        
        latValue = latValue .. latSeconds .. secSymbol
        lonValue = lonValue .. lonSeconds .. secSymbol
      end
      
      latValue = latValue .. latDirection
      lonValue = lonValue .. lonDirection
      
      value = latValue .. separator .. lonValue
      
      if link then
        globe = parseWikidataURL(snak.datavalue.value['globe'])
        
        if globe then
          globe = mw.wikibase.getEntity(globe):getLabel("en"):lower()
        else
          globe = "earth"
        end
        
        value = "[https://tools.wmflabs.org/geohack/geohack.php?language="..self.langCode.."&params="..latitude.."_"..latDirectionEN.."_"..longitude.."_"..lonDirectionEN.."_globe:"..globe.." "..value.."]"
      end
      
      return value
    elseif snak.datavalue.type == 'wikibase-entityid' then
      local lang
      local value = ""
      local title = nil
      local itemID = "Q" .. snak.datavalue.value['numeric-id']
      
      if raw then
        if link then
          return buildWikilink("d:" .. itemID, itemID)
        else
          return itemID
        end
      end
      
      if short then
        value = getShortName(itemID)
      end
      
      if value == "" then
        value, lang = mw.wikibase.getLabelWithLang(itemID)
        
        -- don't allow language fallback
        if lang ~= self.langCode then
          value = nil
        end
      end
      
      if link or value == nil then
        title = mw.wikibase.sitelink(itemID)
      end
      
      if link then
        if title then
          value = buildWikilink(title, (value or title))
        elseif not value then
          value = buildWikilink("d:" .. itemID, itemID)
        end
      elseif not value then
        value = (title or itemID)
      end
      
      return value
    else
      return '<strong class="error">' .. unknownDataTypeError(snak.datavalue.type) .. '</strong>'
    end
  elseif snak.snaktype == 'somevalue' then
    if raw then
      return " "  -- single space represents 'somevalue'
    else
      return i18n['values']['unknown']
    end
  elseif snak.snaktype == 'novalue' 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 == "best" then
    self.bestRank = true
    self.flagBest = true  -- mark that 'best' flag was given
    return
  end
  
  if rank:sub(1,9) == "preferred" then
    rankPos = 1
  elseif rank:sub(1,6) == "normal" then
    rankPos = 2
  elseif rank:sub(1,10) == "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 == "future" then
    periodPos = 1
  elseif period == "current" then
    periodPos = 2
  elseif period == "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:processFlag(flag)
  if not flag then
    return false
  else
    flag = mw.text.trim(flag)
  end
  
  if flag == "linked" then
    self.curState.linked = true
    return true
  elseif flag == "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 == "short" then
    self.curState.shortName = true
    return true
  elseif flag == "mdy" then
    self.mdyDate = true
    return true
  elseif flag == "single" then
    self.singleClaim = true
    return true
  elseif flag == "sourced" then
    self.sourcedOnly = true
    return true
  elseif flag == "best" or flag:match('^preferred[+-]?$') or flag:match('^normal[+-]?$') or flag:match('^deprecated[+-]?$') then
    self:setRank(flag)
    return true
  elseif flag == "future" or flag == "current" or flag == "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
  else
    flag = mw.text.trim(flag)
  end
  
  if flag == "property" or flag == "properties" then
    param = parameters.property
  elseif flag:match('^qualifier[s]?$') 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:match('^reference[s]?$') 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)
  
  -- 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:sub(-1) ~= 's' then
    self.states[param].singleValue = true
  end
  
  self.curState = self.states[param]
  
  return true
end

function Config:qualifierMatches(claim, ID, value)
  local qualifiers
  
  if claim.qualifiers then qualifiers = claim.qualifiers[ID] end
  if qualifiers then
    for i, 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
  
  local now = os.date('!*t')
  
  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)
  elseif endTime == " " then
    -- end time is 'unknown', assume it is somewhere in the past;
    -- we can do this by taking the current date as a placeholder for the end time
    endTimeY = now['year']
    endTimeM = now['month']
    endTimeD = now['day']
  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(now['year'], now['month'], now['day'], startTimeY, startTimeM, startTimeD) then
      return true
    end
  end
  
  if self.periods[2] then
    -- current
    if (startTimeY == nil or not datePrecedesDate(now['year'], now['month'], now['day'], startTimeY, startTimeM, startTimeD)) and
       (endTimeY == nil or datePrecedesDate(now['year'], now['month'], now['day'], endTimeY, endTimeM, endTimeD)) then
      return true
    end
  end
  
  if self.periods[3] then
    -- former
    if endTimeY and not datePrecedesDate(now['year'], now['month'], now['day'], endTimeY, endTimeM, endTimeD) then
      return true
    end
  end
  
  return false
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 = convertRank(claim.rank)
  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 i, 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.conf:getValue(claim.mainsnak, self.rawValue, self.linked, self.shortName)}  -- 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.conf:getValue(snak, self.rawValue, self.linked, self.shortName)}  -- 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
-- logic determined based on https://www.wikidata.org/wiki/Help:Sources
function State:getReference(statement)
  local snakValue, lang, property, url, title
  local value = ""
  local ref = {}
  local params = {}
  local leadParams = {}
  local authors = {}
  local numDetails = 0
  
  if statement.snaks then
    -- don't include "imported from" that has been added by a bot
    if statement.snaks[aliasesP.importedFrom] then
      statement.snaks[aliasesP.importedFrom] = nil
    end
    
    for i in pairs(statement.snaks) do
      numDetails = numDetails + 1
    end
    
    -- use the general template for citing web references if both URL and title are present
    if statement.snaks[aliasesP.referenceURL] and statement.snaks[aliasesP.title] and i18n['cite']['cite-web'] and i18n['cite']['cite-web'] ~= "" then
      params[i18n['cite']['url']]   = self.conf:getValue(statement.snaks[aliasesP.referenceURL][1])
      params[i18n['cite']['title']] = self.conf:getValue(statement.snaks[aliasesP.title][1], false, false, false, true)  -- anyLang = true
      
      if statement.snaks[aliasesP.publicationDate] then params[i18n['cite']['date']]         = self.conf:getValue(statement.snaks[aliasesP.publicationDate][1])                  end
      if statement.snaks[aliasesP.retrieved]       then params[i18n['cite']['access-date']]  = self.conf:getValue(statement.snaks[aliasesP.retrieved][1])                        end
      if statement.snaks[aliasesP.archiveURL]      then params[i18n['cite']['archive-url']]  = self.conf:getValue(statement.snaks[aliasesP.archiveURL][1])                       end
      if statement.snaks[aliasesP.archiveDate]     then params[i18n['cite']['archive-date']] = self.conf:getValue(statement.snaks[aliasesP.archiveDate][1])                      end
      if statement.snaks[aliasesP.publisher]       then params[i18n['cite']['publisher']]    = self.conf:getValue(statement.snaks[aliasesP.publisher][1], false, self.linked)    end  -- link = true/false
      if statement.snaks[aliasesP.quote]           then params[i18n['cite']['quote']]        = self.conf:getValue(statement.snaks[aliasesP.quote][1], false, false, false, true) end  -- anyLang = true
      
      if statement.snaks[aliasesP.author] then
        params[i18n['cite']['author']] = self.conf:getValue(statement.snaks[aliasesP.author][1], false, self.linked)  -- link = true/false
        
        for i = 2, #statement.snaks[aliasesP.author] do
          params[i18n['cite']['author']..i] = self.conf:getValue(statement.snaks[aliasesP.author][i], false, self.linked)  -- link = true/false
        end
      end
      
      if statement.snaks[aliasesP.language] then
        snakValue = self.conf:getValue(statement.snaks[aliasesP.language][1], false, self.linked)  -- link = true/false
        
        if self.conf.langName ~= snakValue then
          params[i18n['cite']['language']] = snakValue
        end
      end
      
      -- 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(params) do
          value = value .. "|" .. i .. "=" .. v
        end
        
        value = "{{" .. i18n['cite']['cite-web'] .. value .. "}}"
      else
        value = mw.getCurrentFrame():expandTemplate{title=i18n['cite']['cite-web'], args=params}
      end
    elseif statement.snaks[aliasesP.statedIn] and numDetails == 1 and i18n['cite']['cite-q'] and i18n['cite']['cite-q'] ~= "" then
      value = self.conf:getValue(statement.snaks[aliasesP.statedIn][1], true)  -- raw = true
      
      if mw.isSubsting() then
        value = "{{" .. i18n['cite']['cite-q'] .. "|" .. value .. "}}"
      else
        value = mw.getCurrentFrame():expandTemplate{title=i18n['cite']['cite-q'], args={value}}
      end
    else
      -- if no general template for citing web references was defined but URL and title are present, add these together
      if statement.snaks[aliasesP.referenceURL] and statement.snaks[aliasesP.title] then
        url   = self.conf:getValue(statement.snaks[aliasesP.referenceURL][1])
        title = self.conf:getValue(statement.snaks[aliasesP.title][1], false, false, false, true)  -- anyLang = true
        
        leadParams[#leadParams + 1] = "[" .. url .. " " .. title .. "]"
        
        -- set to nil so that they won't be added a second time
        statement.snaks[aliasesP.referenceURL] = nil
        statement.snaks[aliasesP.title] = nil
      end
      
      for i, v in pairs(statement.snaks) do
        property = getLabel(i)
        
        if property ~= "" then
          if i == aliasesP.author then
            for j, w in ipairs(v) do
              authors[#authors + 1] = self.conf:getValue(w, false, self.linked)  -- link = true/false
            end
            
            snakValue = table.concat(authors, " & ")
          else
            snakValue, lang = self.conf:getValue(v[1], false, self.linked or (i == aliasesP.statedIn) or (i == aliasesP.referenceURL), false, true)  -- link = true/false, anyLang = true
            
            if lang and lang ~= self.conf.langCode then
              snakValue = "''" .. snakValue .. "'' (" .. mw.language.fetchLanguageName(lang, self.conf.langCode) .. ")"
            end
          end
          
          if i == aliasesP.referenceURL or i == aliasesP.statedIn then
            leadParams[#leadParams + 1] = snakValue
          elseif i ~= aliasesP.language or self.conf.langName ~= snakValue then
            params[#params + 1] = property .. ": " .. snakValue
          end
        end
      end
      
      value = table.concat(leadParams, "; ")
      params = table.concat(params, "; ")
      
      if params ~= "" then
        if value ~= "" then
          value = value .. "; "
        end
        value = value .. params
      end
      
      if value ~= "" then
        value = value .. "."
      end
    end
    
    if value ~= "" then
      ref = {value}  -- create one value object
      
      if not self.rawValue then
        -- this should become a <ref> tag, so safe the reference's hash for later
        ref.refHash = statement.hash
      end
      
      ref = {ref}  -- wrap the value object in an array
    end
  end
  
  return ref
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 i, 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 i2, 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

function p.property(frame)
  loadSubmodules(frame)
  return p._property(copyTable(frame.args))
end

function p._property(args)
  loadSubmodules()
  return execCommand(args, "property")
end

function p.properties(frame)
  loadSubmodules(frame)
  return p._properties(copyTable(frame.args))
end

function p._properties(args)
  loadSubmodules()
  return execCommand(args, "properties")
end

function p.qualifier(frame)
  loadSubmodules(frame)
  return p._qualifier(copyTable(frame.args))
end

function p._qualifier(args)
  loadSubmodules()
  return execCommand(args, "qualifier")
end

function p.qualifiers(frame)
  loadSubmodules(frame)
  return p._qualifiers(copyTable(frame.args))
end

function p._qualifiers(args)
  loadSubmodules()
  return execCommand(args, "qualifiers")
end

function p.reference(frame)
  loadSubmodules(frame)
  return p._reference(copyTable(frame.args))
end

function p._reference(args)
  loadSubmodules()
  return execCommand(args, "reference")
end

function p.references(frame)
  loadSubmodules(frame)
  return p._references(copyTable(frame.args))
end

function p._references(args)
  loadSubmodules()
  return execCommand(args, "references")
end

function execCommand(args, funcName)
  _ = Config.new()
  _:processFlagOrCommand(funcName)  -- process first command (== function name)
  
  local parsedFormat, formatParams, claims, sep
  local hooks = {count = 0}
  
  local nextArg = args[1]
  local nextIndex = 2
  
  -- process flags and commands
  while _:processFlagOrCommand(nextArg) do
    nextArg = args[nextIndex]
    nextIndex = nextIndex + 1
  end
  
  if nextArg then
    nextArg = mw.text.trim(nextArg)
  else
    nextArg = ""
  end
  
  -- check for optional entity ID of either item or property
  if nextArg:sub(1,1):upper() == "Q" then
    _.entity = mw.wikibase.getEntity(nextArg)  -- entity ID of an item given
    _.propertyID = mw.text.trim(args[nextIndex] or "")  -- property ID
    nextIndex = nextIndex + 1
  elseif nextArg:sub(1,9):lower() == "property:" then
    nextArg = replaceAlias(mw.text.trim(nextArg:sub(10)))
    _.entity = mw.wikibase.getEntity(nextArg)  -- entity ID of a property given
    _.propertyID = mw.text.trim(args[nextIndex] or "")  -- property ID
    nextIndex = nextIndex + 1
  else
    _.entity = mw.wikibase.getEntity()  -- no entity ID given, use item connected to current page
    _.propertyID = nextArg  -- property ID
  end
  
  -- check if given property ID is an alias
  _.propertyID = replaceAlias(_.propertyID):upper()
  
  if _.states.qualifiersCount > 0 then
    -- do further processing if "qualifier(s)" command was given
    
    if #args - nextIndex + 1 > _.states.qualifiersCount then
      -- claim ID or literal value has been given
      
      _.propertyValue = mw.text.trim(args[nextIndex])
      nextIndex = nextIndex + 1
    end
    
    for i = 1, _.states.qualifiersCount do
      nextArg = mw.text.trim(args[nextIndex] or "")  -- is a qualifierID
      nextIndex = nextIndex + 1
      
      -- check if given qualifier ID is an alias and add it
      _.qualifierIDs[parameters.qualifier..i] = replaceAlias(nextArg):upper()
    end
  elseif _.states[parameters.reference] then
    -- do further processing if "reference(s)" command was given
    
    nextArg = args[nextIndex]  -- claim ID or literal value (possibly nil)
    nextIndex = nextIndex + 1
    
    if nextArg then
      _.propertyValue = mw.text.trim(nextArg)
    end
  end
  
  -- check for special property value 'somevalue' or 'novalue'
  if _.propertyValue then
    _.propertyValue = replaceSpecialChars(_.propertyValue)
    
    if _.propertyValue ~= "" and mw.text.trim(_.propertyValue) == "" then
      _.propertyValue = " "  -- single space represents 'somevalue', whereas empty string represents 'novalue'
    else
      _.propertyValue = mw.text.trim(_.propertyValue)
    end
  end
  
  -- parse the desired format, or choose an appropriate format
  if args["format"] then
    parsedFormat, formatParams = parseFormat(args["format"])
  elseif _.states.qualifiersCount > 0 then  -- "qualifier(s)" command given
    if _.states[parameters.property] then  -- "propert(y|ies)" command given
      parsedFormat, formatParams = parseFormat(formats.propertyWithQualifier)
    else
      parsedFormat, formatParams = parseFormat(formats.qualifier)
    end
  elseif _.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 _.states.qualifiersCount > 0 and not _.states[parameters.property] then
    _.separators["sep"..parameters.separator][1] = {";"}
  end
  
  -- if only "reference(s)" has been given, set the default separator to none (except when raw)
  if _.states[parameters.reference] and not _.states[parameters.property] and _.states.qualifiersCount == 0
     and not _.states[parameters.reference].rawValue then
    _.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;
  -- must come BEFORE overriding the separator values
  if _.states.qualifiersCount == 1 then
    _.separators["sep"..parameters.qualifier] = _.separators["sep"..parameters.qualifier.."1"]
  end
  
  -- process overridden separator values;
  -- must come AFTER parsing the formats
  for i, v in pairs(_.separators) do
    if args[i] then
      sep = replaceSpecialChars(args[i])
      
      if sep ~= "" then
        _.separators[i][1] = {sep}
      else
        _.separators[i][1] = nil
      end
    end
  end
  
  -- make sure that at least one required parameter has been defined
  if not next(parsedFormat.req) then
    error(missingRequiredParameterError())
  end
  
  -- make sure that the separator parameter "%s" is not amongst the required parameters
  if parsedFormat.req[parameters.separator] then
    error(extraRequiredParameterError(parameters.separator))
  end
  
  -- 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(_.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 _.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 _.states[parameters.property] then
    _.states[parameters.property] = State.new(_)
    
    -- if the "single" flag has been given then this state should be equivalent to "property" (singular)
    if _.singleClaim then
      _.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 _.sourcedOnly and not _.states[parameters.reference] then
    _:processFlagOrCommand("reference")  -- use singular "reference" to minimize overhead
  end
  
  -- set the parsed format and the separators (and optional punctuation mark)
  _.states[parameters.property].parsedFormat = parsedFormat
  _.states[parameters.property].separator = _.separators["sep"]
  _.states[parameters.property].movSeparator = _.separators["sep"..parameters.separator]
  _.states[parameters.property].puncMark = _.separators["punc"]
  
  -- process qualifier matching values, analogous to _.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
      
      _.qualifierIDsAndValues[replaceAlias(i):upper()] = v
    end
  end
  
  if _.entity and _.entity.claims then claims = _.entity.claims[_.propertyID] end
  if claims then
    -- 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
    return concatValues(_.states[parameters.property]:iterate(claims, hooks, State.claimMatches))  -- pass property state with level 1 hooks and matchHook
  else
    return ""
  end
end

function p.label(frame)
  loadSubmodules(frame)
  return p._label(copyTable(frame.args))
end

function p._label(args, _)
  _ = _ or Config.new()
  _.curState = State.new(_)
  
  loadSubmodules()
  
  local ID = nil
  local label = nil
  local title = nil
  local prefix= ""
  local lang
  
  local nextArg = args[1]
  local nextIndex = 2
  
  while _:processFlag(nextArg) do
    nextArg = args[nextIndex]
    nextIndex = nextIndex + 1
  end
  
  if nextArg then
    ID = mw.text.trim(nextArg)
    
    if ID == "" then
      ID = nil
    elseif ID:sub(1,9):lower() == "property:" then
      ID = mw.text.trim(ID:sub(10))
    end
  end
  
  if ID then
    ID = replaceAlias(ID):upper()
    
    -- check if this is a valid ID, and if the number is not larger than max int (to prevent error)
    if not ID:match('^[QP]%d+$') or tonumber(ID:match('%d+')) > 2147483647 then
      return ""
    end
  end
  
  if not _.curState.rawValue or _.pageTitle then
    if not _.pageTitle then
      -- (1) try short name
      if _.curState.shortName then
        if ID and ID:sub(1,1) == "P" then
          label = getShortName("Property:" .. ID)
        else
          label = getShortName(ID)
        end
        
        if label == "" then
          label = nil
        end
      end
      
      -- (2) try label
      if not label then
        label, lang = mw.wikibase.getLabelWithLang(ID)
        
        -- don't allow language fallback
        if lang ~= _.langCode then
          label = nil
        end
      end
    end
    
    -- (3) try sitelink/title
    if not label or _.curState.linked then
      if not ID then
        title = mw.title.getCurrentTitle().prefixedText
      elseif ID:sub(1,1) == "Q" then
        title = mw.wikibase.sitelink(ID)
      elseif label then
        -- properties have no sitelink, 'title = ID' should only be set
        -- if there is a 'label' already and if it needs to be linked;
        -- otherwise, it would be 'label = title == ID' even if '_.pageTitle' is set
        title = ID
        prefix = "d:Property:"
      end
      
      label = label or title
    end
  end
  
  -- (4) try entity ID (i.e. raw value)
  if not label and not _.pageTitle then
    if not ID then
      label = mw.wikibase.getEntityIdForCurrentPage()
    elseif mw.wikibase.getEntity(ID) then
      label = ID
      
      if ID:sub(1,1) == "P" then
        prefix = "Property:"
      end
    end
    
    prefix = "d:" .. prefix
    
    title = label
  end
  
  if not label then
    label = ""
  end
  
  -- add optional link
  if _.curState.linked and title then
    label = buildWikilink(prefix .. title, label)
  end
  
  return label
end

function p.title(frame)
  loadSubmodules(frame)
  return p._title(copyTable(frame.args))
end

function p._title(args, _)
  _ = _ or Config.new()
  _.pageTitle = true
  -- loadSubmodules() will already be called by _label()
  return p._label(args, _)
end

-- main function that is supposed to be used by wrapper templates
function p.main(frame)
  local f, args, i, v
  
  loadSubmodules(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
    error(i18n["errors"]["no-function-specified"])
  end
  
  f = mw.text.trim(frame.args[1])
  
  if f == "main" then
    error(i18n["errors"]["main-called-twice"])
  end
  
  assert(p["_"..f], applyStringParams(i18n['errors']['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