Module:Sandbox/PeaceDeadC/Wd

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

p.commands = {

property = "property",

properties = "properties",

qualifier = "qualifier",

qualifiers = "qualifiers",

reference = "reference",

references = "references",

label = "label",

title = "title",

alias = "alias",

aliases = "aliases"

}

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

plain = "plain"

}

p.args = {

eid = "eid"

}

p.aliasesP = {

coord = "P625",

---------------

author = "P50",

publisher = "P123",

importedFrom = "P143",

statedIn = "P248",

pages = "P304",

publicationDate = "P577",

startTime = "P580",

endTime = "P582",

chapter = "P792",

retrieved = "P813",

referenceURL = "P854",

archiveURL = "P1065",

title = "P1476",

quote = "P1683",

shortName = "P1813",

language = "P2439",

archiveDate = "P2960"

}

local aliasesQ = {

percentage = "Q11229",

prolepticJulianCalendar = "Q1985786",

citeWeb = "Q5637226",

citeQ = "Q22321052"

}

local parameters = {

property = "%p",

qualifier = "%q",

reference = "%r",

alias = "%a",

separator = "%s",

general = "%x"

}

local formats = {

property = "%p[%s][%r]",

qualifier = "%q[%s][%r]",

reference = "%r",

propertyWithQualifier = "%p[ (%q)][%s][%r]",

alias = "%a[%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"}

}

-- default value objects, should NOT be mutated but instead copied

local defaultSeparators = {

["sep"] = {" ", isFormat=true},

["sep%s"] = {",", isFormat=true},

["sep%q"] = {"; ", isFormat=true},

["sep%q\\d"] = {", ", isFormat=true},

["sep%r"] = nil, -- none

["punc"] = nil -- none

}

local singles = {

["&"] = "&",

["|"] = "|",

["<"] = "<",

[">"] = ">",

["%["] = "[",

["%]"] = "]",

}

local doubles = {

["_"] = "_",

["'"] = "'",

["{"] = "{",

["}"] = "}"

}

local encodingInner = {

{"&", singles["&"]}, -- '&' MUST come first

{"|", singles["|"]},

{"<", singles["<"]},

{">", singles[">"]},

{"%[", singles["%["]}, -- needs to be escaped with '%'

{"%]", singles["%]"]}, -- needs to be escaped with '%'

{"_", doubles["_"]},

{"'", doubles["'"]},

{"{", doubles["{"]},

{"}", doubles["}"]}

}

local encodingStart = {

{"=", "="},

{"-", "-"},

{":", ":"},

{"*", "*"},

{"#", "#"},

{";", ";"}

}

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.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.mdyDate = false

cfg.singleClaim = false

cfg.sourcedOnly = false

cfg.plainText = false

cfg.editable = false

cfg.editAtEnd = 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.anyLanguage = false

stt.unitOnly = false

stt.singleValue = false

return stt

end

function preprocess(value, frame)

if mw.isSubsting() then

return value

else

return frame:preprocess(value)

end

end

function replaceAlias(ID)

if p.aliasesP[ID] then

ID = p.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 encodeInnerWikitext(value, skip)

skip = skip or false

if not skip then

for i, v in ipairs(encodingInner) do

value = mw.ustring.gsub(value, v[1], v[2])

end

end

return value

end

function encodeStartWikitext(value, skip)

skip = skip or false

if not skip then

for i, v in ipairs(encodingStart) do

if v[1] == value:sub(1,1) then

value = v[2] .. value:sub(2)

break

end

end

end

return value

end

-- decodes each single, or one of every two equal consecutive, special wiki markup characters

-- that only have a meaning in pairs or larger groups (e.g. italic, bold);

-- this to decrease cluttered output when the module is being substituted;

-- occurences of groups might have arisen during concatenation of different values

function partialDecodeWikitext(value, skip)

skip = skip or false

if not skip then

for i, v in pairs(doubles) do

-- run two rounds to cover all occurences

for i = 1, 2 do

value = mw.ustring.gsub(value, "([^"..i.."])"..v.."([^"..i.."])", "%1"..i.."%2")

end

end

end

return value

end

function buildWikilink(target, label)

if not label or target == label then

return "" .. target .. ""

else

return "" .. 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

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

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

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

-- make sure that at least one required parameter has been defined

if not next(root.req) then

error(missingRequiredParameterError())

end

-- make sure that the separator parameter "%s" is not amongst the required parameters

if root.req[parameters.separator] then

error(extraRequiredParameterError(parameters.separator))

end

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

-- if ID == nil then item connected to current page is used

function getShortName(ID)

local args = {p.flags.plain, p.aliasesP.shortName}

if ID then

args[p.args.eid] = ID

end

return p._property(args) -- "property" is single

end

-- if ID == nil then item connected to current page is used

function Config:getLabel(ID, raw, link, short, plain)

raw = raw or false

link = link or false

short = short or false

plain = plain or false

local label = nil

local title = nil

local prefix= ""

local lang

if raw 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 -- may be nil

else

-- try short name first if requested

if short then

label = getShortName(ID)

if label == "" then

label = nil

end

end

-- get label

if not label then

label, lang = mw.wikibase.getLabelWithLang(ID)

-- don't allow language fallback

if lang ~= self.langCode then

label = nil

end

end

end

if not label then

label = ""

else

label = encodeInnerWikitext(label, plain)

-- build a link if requested

if link then

if not title then

if not ID then

title = mw.title.getCurrentTitle().prefixedText

elseif ID:sub(1,1) == "Q" then

title = mw.wikibase.sitelink(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

end

return label

end

function Config:getEditIcon()

local value = ""

local prefix = ""

local front = " "

local back = ""

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

prefix = "Property:"

end

if self.editAtEnd then

front = ''

back = ''

end

value = "[[File:Blue pencil.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.pageTitle 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

if valuesArray[1] and not valuesArray[1].isFormat then

valuesArray[1][1] = encodeStartWikitext(valuesArray[1][1], self.plainText)

end

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 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 Config:convertUnit(unit, raw, link, short, unitOnly, plain)

raw = raw or false

link = link or false

short = short or false

unitOnly = unitOnly or false

plain = plain or false

local space = " "

local label = ""

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, plain)

if label ~= "" then

return space .. label

end

end

end

return ""

end

function Config:getValue(snak, raw, link, short, anyLang, unitOnly, plain, noSpecial)

raw = raw or false

link = link or false

short = short or false

anyLang = anyLang or false

unitOnly = unitOnly or false

plain = plain or false

noSpecial = noSpecial or false

local value = ""

if snak.snaktype == 'value' then

if snak.datavalue.type == 'string' then

if snak.datatype == 'url' and link then

value = encodeInnerWikitext(snak.datavalue.value, plain)

-- create link explicitly

if raw then

-- will render as a linked number like [1]

return "[" .. value .. "]"

else

return "[" .. value .. " " .. value .. "]"

end

elseif snak.datatype == 'commonsMedia' then

if link then

return buildWikilink("c:File:" .. snak.datavalue.value, encodeInnerWikitext(snak.datavalue.value, plain))

elseif not raw then

return "File:" .. snak.datavalue.value .. ""

else

return encodeInnerWikitext(snak.datavalue.value, plain)

end

elseif snak.datatype == 'geo-shape' and link then

return buildWikilink("c:" .. snak.datavalue.value, encodeInnerWikitext(snak.datavalue.value, plain))

elseif snak.datatype == 'math' and not raw then

return mw.getCurrentFrame():extensionTag("math", snak.datavalue.value)

else

return encodeInnerWikitext(snak.datavalue.value, plain)

end

elseif snak.datavalue.type == 'monolingualtext' then

if anyLang then

return encodeInnerWikitext(snak.datavalue.value['text'], plain), snak.datavalue.value['language']

elseif snak.datavalue.value['language'] == self.langCode then

return encodeInnerWikitext(snak.datavalue.value['text'], plain)

else

return nil

end

elseif snak.datavalue.type == 'quantity' then

local unit

if not unitOnly then

-- get value and strip + signs from front

value = mw.ustring.gsub(snak.datavalue.value['amount'], "^\+(.+)$", "%1")

if raw then

return value

end

-- replace decimal mark based on locale

value = replaceDecimalMark(value)

-- add delimiters for readability

value = addDelimiters(value)

end

unit = self:convertUnit(snak.datavalue.value['unit'], raw, link, short, unitOnly, plain)

if unit then

value = value .. unit

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.."¶ms="..latitude.."_"..latDirectionEN.."_"..longitude.."_"..lonDirectionEN.."_globe:"..globe.." "..value.."]"

end

return value

elseif snak.datavalue.type == 'wikibase-entityid' then

local label

local itemID = snak.datavalue.value['numeric-id']

if snak.datatype == 'wikibase-item' then

itemID = "Q" .. itemID

elseif snak.datatype == 'wikibase-property' then

itemID = "P" .. itemID

else

return '' .. unknownDataTypeError(snak.datatype) .. ''

end

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

if label == "" then

label = nil

end

return label

else

return '' .. unknownDataTypeError(snak.datavalue.type) .. ''

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:processFlag(flag)

if not flag then

return false

else

flag = mw.text.trim(flag)

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.plain then

self.plainText = 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

else

flag = mw.text.trim(flag)

end

if flag == p.commands.property or flag == p.commands.properties then

param = parameters.property

elseif flag == p.commands.qualifier or flag == p.commands.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.commands.reference or flag == p.commands.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)

-- 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, p.aliasesP.startTime)

if startTime and startTime ~= "" and startTime ~= " " then

startTimeY, startTimeM, startTimeD = parseDate(startTime)

end

endTime = self:getSingleRawQualifier(claim, p.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, isFormat=true} -- value object

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, self.anyLanguage, self.unitOnly, self.conf.plainText)} -- 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, self.anyLanguage, self.unitOnly, self.conf.plainText)} -- 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 language, referenceURL, title, statedIn, statedInRaw, template

local authors = {}

local params = {}

local citeParams = {}

local value = ""

local ref = {}

-- number of parameters that do not go along with "stated in"-sources other than web pages as per https://www.wikidata.org/wiki/Help:Sources;

-- these are parameters of properties other than "pages(s)" and "chapter" (for books) and "title" and "publication date" (for databases) and also "stated in"

local hasExtraParams = false

if statement.snaks then

-- don't include "imported from", which has been added by a bot

if statement.snaks[p.aliasesP.importedFrom] then

statement.snaks[p.aliasesP.importedFrom] = nil

end

-- not linked yet because we need the plain value for comparison first

language = self:getReferenceDetail(statement.snaks, p.aliasesP.language, false, false, false, true) -- (noUnset = true)

if language then

-- not part of a "stated in"-source

hasExtraParams = true

-- only add language to the reference if it differs from the local one

if self.conf.langName ~= language then

if self.linked then

-- retrieve language again, but this time with link

params[p.aliasesP.language] = self:getReferenceDetail(statement.snaks, p.aliasesP.language, false, true) -- link = true

else

params[p.aliasesP.language] = language

end

end

-- we have to manually unset, since the first call to getReferenceDetail was with noUnset and the second call might not have happened

statement.snaks[p.aliasesP.language] = nil

end

authors = self:getReferenceDetails(statement.snaks, p.aliasesP.author, false, self.linked) -- link = true/false

if #authors > 0 then

-- not part of a "stated in"-source

hasExtraParams = true

end

referenceURL = self:getReferenceDetail(statement.snaks, p.aliasesP.referenceURL)

if referenceURL then

-- not part of a "stated in"-source

hasExtraParams = true

end

-- the next two may be part of a "stated in"-source, so retrieve them already so that they won't count as hasExtraParams

title = self:getReferenceDetail(statement.snaks, p.aliasesP.title, false, false, true) -- anyLang = true

statedIn = self:getReferenceDetail(statement.snaks, p.aliasesP.statedIn, false, true, false, true) -- link = true, (noUnset = true)

-- get title of general template for citing web references

template = mw.wikibase.sitelink(aliasesQ.citeWeb) or ""

template = split(template, ":")[2] -- split off namespace from front

-- (1) if both "reference URL" and "title" are present, then use the general template for citing web references

if referenceURL and title and template then

citeParams[i18n['cite']['url']] = referenceURL

citeParams[i18n['cite']['title']] = title

citeParams[i18n['cite']['website']] = statedIn

citeParams[i18n['cite']['language']] = params[p.aliasesP.language]

citeParams[i18n['cite']['date']] = self:getReferenceDetail(statement.snaks, p.aliasesP.publicationDate)

citeParams[i18n['cite']['access-date']] = self:getReferenceDetail(statement.snaks, p.aliasesP.retrieved)

citeParams[i18n['cite']['archive-url']] = self:getReferenceDetail(statement.snaks, p.aliasesP.archiveURL)

citeParams[i18n['cite']['archive-date']] = self:getReferenceDetail(statement.snaks, p.aliasesP.archiveDate)

citeParams[i18n['cite']['publisher']] = self:getReferenceDetail(statement.snaks, p.aliasesP.publisher, false, self.linked) -- link = true/false

citeParams[i18n['cite']['quote']] = self:getReferenceDetail(statement.snaks, p.aliasesP.quote, false, false, true) -- anyLang = true

for i, v in ipairs(authors) do

citeParams[i18n['cite']['author']..i] = v

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(citeParams) do

value = value .. "|" .. i .. "=" .. v

end

value = "{{" .. template .. value .. "}}"

else

value = mw.getCurrentFrame():expandTemplate{title=template, args=citeParams}

end

else

-- we need the raw Q-identifier for the next template

statedInRaw = self:getReferenceDetail(statement.snaks, p.aliasesP.statedIn, true) -- raw = true

-- the next three may be part of a "stated in"-source, so retrieve them already so that they won't count as hasExtraParams

params[p.aliasesP.pages] = self:getReferenceDetail(statement.snaks, p.aliasesP.pages)

params[p.aliasesP.chapter] = self:getReferenceDetail(statement.snaks, p.aliasesP.chapter)

params[p.aliasesP.publicationDate] = self:getReferenceDetail(statement.snaks, p.aliasesP.publicationDate)

-- retrieve the rest of the parameters and make them count as hasExtraParams

for i in pairs(statement.snaks) do

params[i] = self:getReferenceDetail(statement.snaks, i, false, self.linked, true) -- link = true/false, anyLang = true

hasExtraParams = true

end

-- get title of template that expands a given stated-in item

template = mw.wikibase.sitelink(aliasesQ.citeQ) or ""

template = split(template, ":")[2] -- split off namespace from front

-- (2) if "stated in" is present without any parameters not belonging to a "stated in"-source, then use the template that expands the stated-in item

if statedInRaw and not hasExtraParams and template then

citeParams[i18n['cite']['pages']] = params[p.aliasesP.pages]

citeParams[i18n['cite']['chapter']] = params[p.aliasesP.chapter]

citeParams[i18n['cite']['date-q']] = params[p.aliasesP.publicationDate]

if mw.isSubsting() then

for i, v in pairs(citeParams) do

value = value .. "|" .. i .. "=" .. v

end

value = "{{" .. template .. "|" .. statedInRaw .. value .. "}}"

else

citeParams[1] = statedInRaw

value = mw.getCurrentFrame():expandTemplate{title=template, args=citeParams}

end

-- (3) else, do some default rendering of name-value pairs, but only if at least "stated in" or "reference URL" is present

elseif statedIn or referenceURL then

-- authors were already retrieved; start by adding them up front

if #authors > 0 then

citeParams[#citeParams + 1] = table.concat(authors, " & ")

end

-- combine "reference URL" and "title" into one link if both are present

if referenceURL and title then

citeParams[#citeParams + 1] = "[" .. referenceURL .. " " .. title .. "]"

elseif referenceURL then

citeParams[#citeParams + 1] = referenceURL

elseif title then

citeParams[#citeParams + 1] = title

end

if statedIn then

citeParams[#citeParams + 1] = statedIn

end

for i, v in pairs(params) do

i = self.conf:getLabel(i)

if i ~= "" then

citeParams[#citeParams + 1] = i .. ": " .. v

end

end

value = table.concat(citeParams, "; ")

if value ~= "" then

value = value .. "."

end

end

end

if value ~= "" then

value = {value} -- create one value object

if not self.rawValue then

-- this should become a tag, so safe the reference's hash for later

value.refHash = statement.hash

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, noUnset)

raw = raw or false

link = link or false

anyLang = anyLang or false

noUnset = noUnset or false

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 i, v in ipairs(snaks[dType]) do

value = self.conf:getValue(v, raw, link, false, anyLang and not switchLang, false, self.conf.plainText, true) -- noSpecial = true

if value then

break

end

end

if value or not anyLang then

break

end

switchLang = not switchLang

until anyLang and switchLang

if not noUnset then

-- remove detail(s) to make it possible to get the rest of the details in one loop

snaks[dType] = nil

end

return value

end

-- gets the details of one particular type for a reference

function State:getReferenceDetails(snaks, dType, raw, link, anyLang, noUnset)

raw = raw or false

link = link or false

anyLang = anyLang or false

noUnset = noUnset or false

local values = {}

if not snaks[dType] then

return {}

end

for i, 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, self.conf.plainText, true) -- noSpecial = true

end

if not noUnset then

-- remove detail(s) to make it possible to get the rest of the details in one loop

snaks[dType] = nil

end

return values

end

-- level 1 hook

function State:getAlias(object)

local value = object.value

local title = nil

if value and self.linked then

value = encodeInnerWikitext(value, self.conf.plainText)

if self.conf.entityID:sub(1,1) == "Q" then

title = mw.wikibase.sitelink(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

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 preprocess(p._property(copyTable(frame.args)), frame)

end

function p._property(args)

loadSubmodules()

return valueCommand(args, p.commands.property)

end

function p.properties(frame)

loadSubmodules(frame)

return preprocess(p._properties(copyTable(frame.args)), frame)

end

function p._properties(args)

loadSubmodules()

return valueCommand(args, p.commands.properties)

end

function p.qualifier(frame)

loadSubmodules(frame)

return preprocess(p._qualifier(copyTable(frame.args)), frame)

end

function p._qualifier(args)

loadSubmodules()

return valueCommand(args, p.commands.qualifier)

end

function p.qualifiers(frame)

loadSubmodules(frame)

return preprocess(p._qualifiers(copyTable(frame.args)), frame)

end

function p._qualifiers(args)

loadSubmodules()

return valueCommand(args, p.commands.qualifiers)

end

function p.reference(frame)

loadSubmodules(frame)

return preprocess(p._reference(copyTable(frame.args)), frame)

end

function p._reference(args)

loadSubmodules()

return valueCommand(args, p.commands.reference)

end

function p.references(frame)

loadSubmodules(frame)

return preprocess(p._references(copyTable(frame.args)), frame)

end

function p._references(args)

loadSubmodules()

return valueCommand(args, p.commands.references)

end

function valueCommand(args, funcName)

local _ = Config.new()

_:processFlagOrCommand(funcName) -- process first command (== function name)

local parsedFormat, formatParams, claims, sep, value

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

_.entityID = nextArg:upper() -- entity ID of an item was given

_.entity = mw.wikibase.getEntity(_.entityID) -- get the item-entity based on the given ID

_.propertyID = mw.text.trim(args[nextIndex] or "") -- property ID

nextIndex = nextIndex + 1

elseif nextArg:sub(1,9):lower() == "property:" then

_.entityID = replaceAlias(mw.text.trim(nextArg:sub(10))):upper() -- entity ID of a property was given

_.entity = mw.wikibase.getEntity(_.entityID) -- get the property-entity based on the given ID

_.propertyID = mw.text.trim(args[nextIndex] or "") -- property ID

nextIndex = nextIndex + 1

else

-- no positional entity ID was given, so get entity ID from 'eid' if it was given

if args[p.args.eid] then

if args[p.args.eid]:sub(1,1):upper() == "Q" then

_.entityID = args[p.args.eid]:upper()

elseif args[p.args.eid]:sub(1,9):lower() == "property:" then

_.entityID = replaceAlias(mw.text.trim(args[p.args.eid]:sub(10))):upper()

else

-- might be entity ID of a property, or a bogus entity ID that will fail later on

_.entityID = replaceAlias(args[p.args.eid]):upper()

if _.entityID == "" then

-- explicitly return if value for 'eid' is empty

return ""

end

end

else

_.entityID = mw.wikibase.getEntityIdForCurrentPage() -- by default, use item-entity connected to current page

end

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

_.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 format

for i, v in pairs(_.separators) do

if args[i] then

sep = replaceSpecialChars(args[i])

if sep ~= "" then

_.separators[i][1] = {sep, isFormat=true}

else

_.separators[i][1] = nil

end

end

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(p.commands.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 p.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

value = _:concatValues(_.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 _.editable and value ~= "" then

value = value .. _:getEditIcon()

end

return partialDecodeWikitext(value, _.plainText)

else

return ""

end

end

function p.label(frame)

loadSubmodules(frame)

return preprocess(p._label(copyTable(frame.args)), frame)

end

function p._label(args)

loadSubmodules()

return nameCommand(args, p.commands.label)

end

function p.title(frame)

loadSubmodules(frame)

return preprocess(p._title(copyTable(frame.args)), frame)

end

function p._title(args)

loadSubmodules()

return nameCommand(args, p.commands.title)

end

function p.alias(frame)

loadSubmodules(frame)

return preprocess(p._alias(copyTable(frame.args)), frame)

end

function p._alias(args)

loadSubmodules()

return nameCommand(args, p.commands.alias)

end

function p.aliases(frame)

loadSubmodules(frame)

return preprocess(p._aliases(copyTable(frame.args)), frame)

end

function p._aliases(args)

loadSubmodules()

return nameCommand(args, p.commands.aliases)

end

function nameCommand(args, funcName)

local _ = Config.new()

_.curState = State.new(_)

local ID = nil

local value = nil

local nextArg = args[1]

local nextIndex = 2

while _:processFlag(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

ID = nextArg:upper() -- entity ID of an item was given

elseif nextArg:sub(1,9):lower() == "property:" then

ID = replaceAlias(mw.text.trim(nextArg:sub(10))):upper() -- entity ID of a property was given

else

-- might be entity ID of a property, or a bogus entity ID that will fail later on

ID = replaceAlias(nextArg):upper()

if ID:sub(1,1) ~= "P" then

if ID == "" then

-- no positional entity ID was given, so get entity ID from 'eid' if it was given

if args[p.args.eid] then

if args[p.args.eid]:sub(1,1):upper() == "Q" then

ID = args[p.args.eid]:upper()

elseif args[p.args.eid]:sub(1,9):lower() == "property:" then

ID = replaceAlias(mw.text.trim(args[p.args.eid]:sub(10))):upper()

else

-- might be entity ID of a property, or a bogus entity ID that will fail later on

ID = replaceAlias(args[p.args.eid]):upper()

if ID == "" then

-- explicitly return if value for 'eid' is empty

return ""

end

end

else

ID = nil

end

end

end

end

if ID then

_.entityID = ID

else

_.entityID = mw.wikibase.getEntityIdForCurrentPage()

end

-- serve according to the given command

if funcName == p.commands.label then

value = encodeStartWikitext(_:getLabel(ID, _.curState.rawValue, _.curState.linked, _.curState.shortName, _.plainText), _.plainText)

elseif funcName == p.commands.title then

_.pageTitle = true

if not ID then

value = mw.title.getCurrentTitle().prefixedText

elseif ID:sub(1,1) == "Q" then

value = mw.wikibase.sitelink(ID)

end

if value then

if _.curState.linked then

value = buildWikilink(value, encodeInnerWikitext(value, _.plainText))

else

value = encodeStartWikitext(encodeInnerWikitext(value, _.plainText), _.plainText)

end

end

elseif funcName == p.commands.alias or funcName == p.commands.aliases then

local aliases, parsedFormat, formatParams, sep

local hooks = {count = 0}

if funcName == p.commands.alias then

_.curState.singleValue = true

end

-- 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 parsing the format

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

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

_.curState.parsedFormat = parsedFormat

_.curState.separator = _.separators["sep"]

_.curState.movSeparator = _.separators["sep"..parameters.separator]

_.curState.puncMark = _.separators["punc"]

_.entity = mw.wikibase.getEntity(ID)

if _.entity and _.entity.aliases then aliases = _.entity.aliases[_.langCode] end

if aliases then

value = _:concatValues(_.curState:iterate(aliases, hooks))

end

end

value = value or ""

if _.editable and value ~= "" then

-- if desired, add a clickable icon that may be used to edit the returned value on Wikidata

value = value .. _:getEditIcon()

end

return partialDecodeWikitext(value, _.plainText)

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 preprocess(p["_"..f](args), frame)

end

return p