انتقل إلى المحتوى

وحدة:WdItem

من ويكاموس، القاموس الحر
-- Module: WdItem
-- Object-oriented Wikidata access module for Lua
-- Refactored from WikidataIB for direct Lua usage

require("strict")
local p = {}

-- i18n configuration
local i18n = {
	["errors"] = {
		["property-not-found"] = "الخاصية غير موجودة.",
		["entity-not-found"] = "الكيان على ويكي بيانات غير موجود.",
		["unknown-claim-type"] = "نوع الادعاء غير معروف.",
		["local-article-not-found"] = "المعرف متاح على ويكي بيانات، ولكن المقال غير متاح على ويكيبيديا",
	},
	["months"] = {
		"January", "February", "March", "April", "May", "June",
		"July", "August", "September", "October", "November", "December"
	},
	["Unknown"] = "Unknown",
	["NaN"] = "Not a number",
	["missinginfocat"] = "[[تصنيف:Articles with missing Wikidata information]]",
	["list separator"] = ", ",
	["list separator-ar"] = "، ",
}

-- Date precision constants
local dp = {
	[6] = "millennium",
	[7] = "century",
	[8] = "decade",
	[9] = "year",
	[10] = "month",
	[11] = "day",
}

local options_aliases = {
	["osd"] = "onlysourced",
	["wdlinks"] = "wdl",
	["uabbr"] = "unitabbr",
	["qo"] = "qualsonly",
	["lp"] = "linkprefix",
	["lprefix"] = "linkprefix",
	["qlp"] = "qlinkprefix",
	["sn"] = "shortname",
	["uselabel"]  = "uselbl",
	["plaindate"] = "pd",
	["qualifierdateformat"] = "qdf",
	["df"] = "qdf",
	["qshortname"] = "qsn"
}

setmetatable(options_aliases, { __index = function(t, k) return k end })

-------------------------------------------------------------------------------
-- Internal helper functions
-------------------------------------------------------------------------------

local function findLang(langcode)
	local langobj
	langcode = mw.text.trim(langcode or "")
	if mw.language.isKnownLanguageTag(langcode) then
		langobj = mw.language.new(langcode)
	else
		langcode = mw.getCurrentFrame():callParserFunction('int', {'lang'})
		if mw.language.isKnownLanguageTag(langcode) then
			langobj = mw.language.new(langcode)
		else
			langobj = mw.language.getContentLanguage()
		end
	end
	return langobj
end

local function parseParam(param, default)
	if param == nil then
		return default
	end
	return param
end

local function setRanks(rank)
	rank = (rank or ""):lower()
	local ranks = {}
	for w in string.gmatch(rank, "%a+") do
		w = w:sub(1,1)
		if w == "b" or w == "p" or w == "n" or w == "d" then
			ranks[w] = true
		end
	end
	if ranks.b or not next(ranks) then
		ranks.p = true
		ranks.n = true
	end
	return ranks
end

local function labelOrId(id, lang)
	if lang == "default" then lang = findLang().code end
	local label
	if lang then
		label = mw.wikibase.getLabelByLang(id, lang)
	end
	if not label then
		local trylang = {"ar","en","fr"}
		local triedlang = lang
		for _,lang in pairs(trylang) do
			if triedlang ~= lang then
				label = mw.wikibase.getLabelByLang(id, lang)
				if label ~= "" then
					break
				end
			end	
		end
	end

	if label then
		return mw.text.nowiki(label), true
	else
		return id, false
	end
end

local function roundto(x, sf)
	if x == 0 then return 0 end
	local s = 1
	if x < 0 then
		x = -x
		s = -1
	end
	if sf < 1 then sf = 1 end
	local p = 10 ^ (math.floor(math.log10(x)) - sf + 1)
	x = math.floor(x / p + 0.5) * p * s
	if x == math.floor(x) then x = math.floor(x) end
	return x
end

local function decimalPrecision(x, p)
	local s = 1
	if x < 0 then
		x = -x
		s = -1
	end
	if not tonumber(p) then p = 1e-4
	elseif p > 1 then p = 1
	elseif p < 1e-6 then p = 1e-6
	else p = 10 ^ math.floor(math.log10(p))
	end
	x = math.floor(x / p + 0.5) * p * s
	if  x == math.floor(x) then x = math.floor(x) end
	if math.abs(x) < 1e-4 then x = string.format("%f", x) end
	return x
end

local function decimalToDMS(x, p)
	if not tonumber(p) then p = 1e-4 end
	local d = math.floor(x)
	local ms = (x - d) * 60
	if p > 0.5 then
		if ms > 30 then d = d + 1 end
		ms = 0
	end
	local m = math.floor(ms)
	local s = (ms - m) * 60
	if p > 0.008 then
		if s > 30 then m = m +1 end
		s = 0
	elseif p > 0.00014 then
		s = math.floor(s + 0.5)
	elseif p > 0.000014 then
		s = math.floor(10 * s + 0.5) / 10
	elseif p > 0.0000014 then
		s = math.floor(100 * s + 0.5) / 100
	else
		s = math.floor(1000 * s + 0.5) / 1000
	end
	return d, m, s
end

local function dateFormat(timestamp, dprec, df, bcf, pd, qualifiers, lang, adj, model)
	df = (df or ""):lower()
	if df == "ymd" then
		if timestamp:sub(1,1) == "+" then
			return timestamp:sub(2,11)
		else
			local yr = tonumber(timestamp:sub(2,5)) - 1
			yr = ("000" .. yr):sub(-4)
			if yr ~= "0000" then yr = "-" .. yr end
			return yr .. timestamp:sub(6,11)
		end
	end
	timestamp = timestamp:gsub("%-00%-00T", "-01-01T")
	dprec = dprec or 11
	if df == "y" and dprec > 9 then dprec = 9 end
	dprec = dprec>11 and 11 or dprec
	dprec = dprec<6 and 6 or dprec
	bcf = (bcf or ""):upper()
	pd = (pd or ""):sub(1,1):lower()
	if pd == "" or pd == "n" or pd == "f" or pd == "0" then pd = false end
	lang = lang or findLang().code
	adj = adj or ""
	
	local bc = timestamp:sub(1, 1)=="-" and "BC" or ""
	local year, month, day = timestamp:match("[+-](%d*)-(%d*)-(%d*)T")
	local iso = tonumber(year)
	if dprec == 6 then iso = math.floor( (iso - 1) / 1000 ) + 1 end
	if dprec == 7 then iso = math.floor( (iso - 1) / 100 ) + 1 end
	if dprec == 8 then iso = math.floor( iso / 10 ) .. "0" end
	if dprec == 10 then iso = year .. "-" .. month end
	if dprec == 11 then iso = year .. "-" .. month .. "-" .. day end
	
	local sc = not pd and qualifiers and qualifiers.P1480
	if sc then
		for k1, v1 in pairs(sc) do
			if v1.datavalue and v1.datavalue.value.id == "Q5727902" then
				adj = "circa"
				break
			end
		end
	end
	
	local calendarmodel = ""
	if tonumber(year) > 1582 and dprec > 8 and not pd and model == "http://www.wikidata.org/entity/Q1985786" then
		calendarmodel = "julian"
	end
	
	-- Simplified date formatting (complex date module not available)
	local fdate = tostring(iso)
	if bc ~= "" then
		fdate = fdate .. " " .. (bcf == "BC" and "BC" or "BCE")
	end
	if adj == "circa" then
		fdate = "c. " .. fdate
	end
	
	return fdate
end

local function sourced(claim)
	if claim.references then
		for kr, vr in pairs(claim.references) do
			local ref = mw.wikibase.renderSnaks(vr.snaks)
			if not ref:find("Wiki") then
				return true
			end
		end
	end
end

local function linkedItem(id, args)
	local lprefix = (args.linkprefix or ""):gsub('"', '')
	local lpostfix = (args.lpostfix or ""):gsub('"', '')
	local prefix = (args.prefix or ""):gsub('"', '')
	local postfix = (args.postfix or ""):gsub('"', '')
	local dtxt = args.dtxt
	local lang = args.lang or "ar"
	local uselbl = parseParam( args.uselbl, true)
	
	local disp
	local sitelink = mw.wikibase.getSitelink(id)
	local label, islabel
	if dtxt then
		label, islabel = dtxt, true
	else
		label, islabel = labelOrId(id, args.lang)
	end
	
	if sitelink then
		if not uselbl then
			local pos = sitelink:find(":") or 0
			local slink = sitelink
			if pos > 0 then
				local pfx = sitelink:sub(1,pos-1)
				if mw.site.namespaces[pfx] then
					slink = sitelink:sub(pos+1)
				end
			end
			slink = slink:gsub("%s%(.+%)$", ""):gsub(",.+$", "")
			if label:find("^%u") then
				label = slink:gsub("^(%l)", string.upper)
			else
				label = slink:gsub("^(%u)", string.lower)
			end
		end
		disp = "[[" .. lprefix .. sitelink .. lpostfix .. "|" .. prefix .. label .. postfix .. "]]"
	elseif islabel then
		disp = prefix .. label .. postfix
	else
		disp = prefix .. label .. postfix .. i18n.missinginfocat
	end
	
	return disp
end

local function rendersnak(propval, args, linked, lpre, lpost, pre, post, uabbr, filter)
	lpre = lpre or ""
	lpost = lpost or ""
	pre = pre or ""
	post = post or ""
	args.lang = args.lang or findLang().code
	
	local snak = propval.mainsnak or propval
	local dtype = snak.datatype
	local dv = snak.datavalue
	dv = dv and dv.value
	local val, mlt
	
	if propval.rank and not args.reqranks[propval.rank:sub(1, 1)] then
		return nil
	elseif snak.snaktype == "somevalue" then
		val = i18n["Unknown"]
	elseif snak.snaktype == "novalue" then
		-- return nothing
	elseif dtype == "wikibase-item" then
		local qnumber = dv.id
		if linked then
			val = linkedItem(qnumber, args)
		else
			local label, islabel = labelOrId(qnumber, args.lang)
			val = pre .. label .. post
		end
	elseif dtype == "time" then
		val = dateFormat(dv.time, dv.precision, args.df, args.bc, args.pd, propval.qualifiers, args.lang, "", dv.calendarmodel)
	elseif dtype == "commonsMedia" or dtype == "external-id" or dtype == "string" or dtype == "url" then
		if (lpre == "" or lpre == ":") and lpost == "" then
			val = pre .. dv .. post
		elseif dtype == "external-id" then
			val = "[" .. lpre .. dv .. lpost .. " " .. pre .. dv .. post .. "]"
		else
			val = "[[" .. lpre .. dv .. lpost .. "|" .. pre .. dv .. post .. "]]"
		end
	elseif dtype == "quantity" then
		local amount = tonumber(dv.amount) or i18n["NaN"]
		local unit = ""
		local unitqid = string.match(dv.unit, "(Q%d+)")
		if filter and unitqid ~= filter then return nil end
		if unitqid then
			local uname = mw.wikibase.getLabelByLang(unitqid, args.lang) or ""
			if uname ~= "" then unit = " " .. uname end
		end
		val = tostring(amount) .. unit
	elseif dtype == "globe-coordinate" then
		local lat, long, prec = dv.latitude, dv.longitude, dv.precision
		local show = (args.show or ""):lower()
		if show == "lat" then
			val = decimalPrecision(lat, prec)
		elseif show == "lon" then
			val = decimalPrecision(long, prec)
		elseif show == "longlat" then
			val = decimalPrecision(long, prec) .. ", " .. decimalPrecision(lat, prec)
		else
			local ns = lat < 0 and "S" or "N"
			local ew = long < 0 and "W" or "E"
			if lat < 0 then lat = -lat end
			if long < 0 then long = -long end
			val = decimalPrecision(lat, prec) .. "°" .. ns .. " " .. decimalPrecision(long, prec) .. "°" .. ew
		end
	elseif dtype == "monolingualtext" then
		val = pre .. dv.text .. post
		mlt = dv.language
	else
		val = "unknown data type: " .. dtype
	end
	
	return val, mlt
end

local function assembleoutput(out, args, entityID, propertyID)
	local sorted = parseParam(args.sorted, false)
	local noic = parseParam(args.noicon, false)
	local list = args.list or ""
	local separator = args.sep or ""
	separator = string.gsub(separator, '"', '')
	
	if separator == "" then
		local rtl_lang = {ar = true, fa = true, ur = true, ku = true, he = true}
		separator = rtl_lang[args.lang] and i18n["list separator-ar"] or i18n["list separator"]
	end
	
	local strout
	if #out > 0 then
		if sorted then table.sort(out) end
		
		if list == "" then
			strout = table.concat(out, separator)
		elseif list:lower() == "prose" then
			strout = mw.text.listToText(out)
		else
			strout = mw.getCurrentFrame():expandTemplate{title = list, args = out}
		end
	else
		strout = nil
	end
	
	return strout
end

local function propertyvalueandquals(objproperty, args, qualID)
	-- needs this style of declaration because it's re-entrant

	-- onlysourced is a boolean passed to return only values sourced to other than Wikipedia
	-- if nothing or an empty string is passed set it true
	local onlysrc = parseParam(args.onlysourced, false)

	-- linked is a a boolean that enables the link to a local page via sitelink
	-- if nothing or an empty string is passed set it true
	local linked = parseParam(args.linked, true)

	-- prefix is a string that may be nil, empty (""), or a string of characters
	-- this is prefixed to each value
	-- useful when when multiple values are returned
	-- any double-quotes " are stripped out, so that spaces may be passed
	local prefix = (args.prefix or ""):gsub('"', '')

	-- postfix is a string that may be nil, empty (""), or a string of characters
	-- this is postfixed to each value
	-- useful when when multiple values are returned
	-- any double-quotes " are stripped out, so that spaces may be passed
	local postfix = (args.postfix or ""):gsub('"', '')

	-- linkprefix is a string that may be nil, empty (""), or a string of characters
	-- this creates a link and is then prefixed to each value
	-- useful when when multiple values are returned and indirect links are needed
	-- any double-quotes " are stripped out, so that spaces may be passed
	local lprefix = (args.linkprefix or ""):gsub('"', '')

	-- linkpostfix is a string that may be nil, empty (""), or a string of characters
	-- this is postfixed to each value when linking is enabled with lprefix
	-- useful when when multiple values are returned
	-- any double-quotes " are stripped out, so that spaces may be passed
	local lpostfix = (args.linkpostfix or ""):gsub('"', '')

	-- wdlinks is a boolean passed to enable links to Wikidata when no article exists
	-- if nothing or an empty string is passed set it false
	local wdl = parseParam(args.wdl, false)

	-- unitabbr is a boolean passed to enable unit abbreviations for common units
	-- if nothing or an empty string is passed set it false
	local uabbr = parseParam(args.unitabbr, false)

	-- qualsonly is a boolean passed to return just the qualifiers
	-- if nothing or an empty string is passed set it false
	local qualsonly = parseParam(args.qualsonly, false)

	-- maxvals is a string that may be nil, empty (""), or a number
	-- this determines how many items may be returned when multiple values are available
	-- setting it = 1 is useful where the returned string is used within another call, e.g. image
	local maxvals = tonumber(args.maxvals) or 0

	-- pd (plain date) is a string: yes/true/1 | no/false/0 | adj
	-- to disable/enable "sourcing cirumstances" or use adjectival form for the plain date
	local pd = args.pd or "no"
	args.pd = pd

	-- allow qualifiers to have a different date format; default to year unless qualsonly is set
	args.qdf = args.qdf or (not qualsonly and "y")

	local lang = args.lang or (args.langs and args.langs[1]) or findLang().code

	-- qualID is a string list of wanted qualifiers or "ALL"
    qualID = qualID or ""
    -- capitalise list of wanted qualifiers and substitute "DATES"
    qualID = qualID:upper():gsub("DATES", "P580, P582")
    local allflag = (qualID == "ALL")
    -- create table of wanted qualifiers as key
    local qwanted = {}
    -- create sequence of wanted qualifiers
    local qorder = {}
    for q in mw.text.gsplit(qualID, "%p") do -- split at punctuation and iterate
        local qtrim = mw.text.trim(q)
        if qtrim ~= "" then
            qwanted[mw.text.trim(q)] = true
            qorder[#qorder+1] = qtrim
        end
    end
    -- qsep is the output separator for rendering qualifier list
    local qsep = (args.qsep or ""):gsub('"', '')
    -- qargs are the arguments to supply to assembleoutput()
    local qargs = {
        ["osd"]         = "false",
        ["linked"]      = tostring(linked),
        ["prefix"]      = args.qprefix,
        ["postfix"]     = args.qpostfix,
        ["linkprefix"]  = args.qlinkprefix,
        ["linkpostfix"] = args.qlinkpostfix,
        ["wdl"]         = "false",
        ["unitabbr"]    = tostring(uabbr),
        ["maxvals"]     = 0,
        ["sorted"]      = tostring(args.qsorted),
        ["noicon"]      = "true",
        ["list"]        = args.qlist,
        ["sep"]         = qsep,
        -- ["langobj"]     = args.langobj,
        ["lang"]        = args.lang or (args.langs and args.lang[1]),
        ["df"]          = args.qdf,
        ["sn"]          = parseParam(args.qsn, false),
    }

	-- all proper values of a Wikidata property will be the same type as the first
	-- qualifiers don't have a mainsnak, properties do
	--#Modified line
	local datatype = objproperty[1].datatype or (objproperty[1].mainsnak and objproperty[1].mainsnak.datatype)

	-- out[] holds the a list of returned values for this property
	-- mlt[] holds the language code if the datatype is monolingual text
	local out = {}
	local mlt = {}

	for k, v in ipairs(objproperty) do
		local hasvalue = true
		if (onlysrc and not sourced(v)) then
			-- no value: it isn't sourced when onlysourced=true
			hasvalue = false
		else
			local val, lcode = rendersnak(v, args, linked, lprefix, lpostfix, prefix, postfix, uabbr)
			if not val then
				hasvalue = false -- rank doesn't match
			elseif qualsonly and qualID then
				-- suppress value returned: only qualifiers are requested
			else
				out[#out+1], mlt[#out+1] = val, lcode
			end
		end

		-- See if qualifiers are to be returned:
		local snak = v.mainsnak or v
		if hasvalue and v.qualifiers and qualID ~= "" and snak.snaktype~="novalue" then
            -- collect all wanted qualifier values returned in qlist, indexed by propertyID
			local qlist = {}
			local timestart, timeend = "", ""
            -- loop through qualifiers
            for k1, v1 in pairs(v.qualifiers) do
                if allflag or qwanted[k1] then
                    if k1 == "P1326" then
                        local ts = v1[1].datavalue.value.time
                        local dp = v1[1].datavalue.value.precision
                        qlist[k1] = dateFormat(ts, dp, args.qdf, args.bc, pd, "", lang, "before")
                    elseif k1 == "P1319" then
                        local ts = v1[1].datavalue.value.time
                        local dp = v1[1].datavalue.value.precision
                        qlist[k1] = dateFormat(ts, dp, args.qdf, args.bc, pd, "", lang, "after")
                    elseif k1 == "P580" then
                        timestart = propertyvalueandquals(v1, qargs)[1] or "" -- treat only one start time as valid
                    elseif k1 == "P582" then
                        timeend = propertyvalueandquals(v1, qargs)[1] or "" -- treat only one end time as valid
                    else
                        local q = assembleoutput(propertyvalueandquals(v1, qargs), qargs)
                        -- we already deal with circa via 'sourcing circumstances' if the datatype was time
                        -- circa may be either linked or unlinked *** internationalise later ***
                        if datatype ~= "time" or q ~= "circa" and not (type(q) == "string" and q:find("circa]]")) then
                            qlist[k1] = q
                        end
                    end
                end -- of test for wanted
            end -- of loop through qualifiers
            -- set date separator
			local t = timestart .. timeend
			-- *** internationalise date separators later ***
			local dsep = "&ndash;"
			if t:find("%s") or t:find("&nbsp;") then dsep = " &ndash; " end
            -- set the order for the list of qualifiers returned; start time and end time go last
			if next(qlist) then
                local qlistout = {}
                if allflag then
                    for k2, v2 in pairs(qlist) do
                        qlistout[#qlistout+1] = v2
                    end
                else
                    for i2, v2 in ipairs(qorder) do
                        qlistout[#qlistout+1] = qlist[v2]
                    end
                end
                if t ~= "" then
                    qlistout[#qlistout+1] = timestart .. dsep .. timeend
                end
				local qstr = assembleoutput(qlistout, qargs)
				if qualsonly then
					out[#out+1] = qstr
				else
					out[#out] = out[#out] .. " (" .. qstr .. ")"
				end
			elseif t ~= "" then
				if qualsonly then
					if timestart == "" then
						out[#out+1] = timeend
					elseif timeend == "" then
						out[#out+1] = timestart
					else
						out[#out+1] = timestart .. dsep .. timeend
					end
				else
					out[#out] = out[#out] .. " (" .. timestart .. dsep .. timeend .. ")"
				end
			end
		end -- of test for qualifiers wanted

		if maxvals > 0 and #out >= maxvals then break end
	end -- of for each value loop

	-- we need to pick one value to return if the datatype was "monolingualtext"
	-- if there's only one value, use that
	-- otherwise look through the fallback languages for a match
	if datatype == "monolingualtext" and #out >1 then
		lang = mw.text.split( lang, '-', true )[1]
		local fbtbl = mw.language.getFallbacksFor( lang )
		table.insert( fbtbl, 1, lang )
		local bestval = ""
		local found = false
		for idx1, lang1 in ipairs(fbtbl) do
			for idx2, lang2 in ipairs(mlt) do
				if (lang1 == lang2) and not found then
					bestval = out[idx2]
					found = true
					break
				end
			end -- loop through values of property
		end -- loop through fallback languages
		if found then
			-- replace output table with a table containing the best value
			out = { bestval }
		else
			-- more than one value and none of them on the list of fallback languages
			-- sod it, just give them the first one
			out = { out[1] }
		end
	end
	return out
end
-------------------------------------------------------------------------------
-- WdItem Class Definition
-------------------------------------------------------------------------------
local function mergeOptions(defaults, overrides)
	overrides = overrides or {}
	local result = {}
	for k, v in pairs(defaults) do
		result[k] = v
	end
	for k, v in pairs(overrides) do
		result[k] = v
	end
	return result
end

local WdItem = {}
WdItem.__index = WdItem

function WdItem:__tostring()
	return "WdItem: " .. (self.qid or "no QID")
end

function WdItem:UpdateOptions(newOpts)
	for k, v in pairs(newOpts or {}) do
		local key = options_aliases[k]
		self.options[key] = v
			-- Set default options
		if key == 'langs' and #v > 0 then
			self.options.langobj = findLang(self.options.langs[1])
			self.options.lang = self.options.langobj.code

			if not self.options.langobj then
				self.options.langobj = findLang(self.options.lang)
				self.options.lang = self.options.langobj.code
			end
		end

		if key == 'rank' then
			self.options.reqranks = setRanks(self.options.rank)
		end
	end
	if not self.options.reqranks then
		self.options.reqranks = setRanks('best')
	end

end


function WdItem.new(qid, options)
	local self = setmetatable({}, WdItem)
	if qid == "" then qid = nil end
	if (not qid) and mw.wikibase then
		qid = mw.wikibase.getEntityIdForCurrentPage()
	end
	self.qid = qid
	self.snak = nil
	self.options = options or {}
	
	self:UpdateOptions(options)
	
	return self
end

function WdItem.newFromSnaks(snakTable, options)
	local self = setmetatable({}, WdItem)
	self.qid = nil
	self.snak = snakTable or {}
	self.options = options or {}
	self:UpdateOptions(options)

	return self
end

function WdItem:setOptions(newOpts)
	self:UpdateOptions(newOpts)
end

function WdItem:getStatements(pid, opts)
	opts = opts or {}
	local vol = opts.vol
	local chapter = opts.chapter
	
	if self.snak and self.snak[pid] then
		return self.snak[pid]
	end
	
	if not self.qid or not mw.wikibase then
		return {}
	end
	
	if vol or chapter then
		return p.getStatementsByVol(self.qid, pid, vol, chapter) or {}
	end
	
	if self.options.reqranks and self.options.reqranks.b then
		return mw.wikibase.getBestStatements(self.qid, pid) or {}
	else
		return mw.wikibase.getAllStatements(self.qid, pid) or {}
	end
end

function WdItem:getValue(pid, opts)
	local mergedOpts = mergeOptions(self.options, opts)
	
	-- Handle local value override
	if mergedOpts[2] and mergedOpts[2] ~= "" then
		return mergedOpts[2]
	end
	
	local statements
	if mergedOpts.no_vol_qual then
		statements = mw.wikibase.getBestStatements(self.qid, pid)
	else
		statements = self:getStatements(pid, {vol = mergedOpts.vol, chapter = mergedOpts.chapter})
	end
	
	if not statements or #statements == 0 then
		return nil
	end
	
	local qualID = mw.text.trim(mergedOpts.qual or ""):upper()
	if qualID == "" then qualID = nil end
	
	local out = propertyvalueandquals(statements, mergedOpts, qualID)
	return assembleoutput(out, mergedOpts, self.qid, pid)
end

function WdItem:getValueFromSnaks(pid, opts)
	if not self.snak or not self.snak[pid] then
		return nil
	end

	local mergedOpts = mergeOptions(self.options, opts)
	
	local props = self.snak[pid]
	local out = propertyvalueandquals(props, mergedOpts)
	return assembleoutput(out, mergedOpts, nil, pid)
end

function WdItem:getPropertyIDs(pid, opts)
	local mergedOpts = mergeOptions(self.options, opts)

	mergedOpts.noicon = tostring(parseParam(mergedOpts.noicon or "", true))
	
	local statements = self:getStatements(pid, opts)
	if not statements or #statements == 0 then return nil end
	
	local onlysrc = parseParam(mergedOpts.onlysourced or mergedOpts.osd, true)
	local maxvals = tonumber(mergedOpts.maxvals) or 0
	
	local out = {}
	for i, v in ipairs(statements) do
		local snak = v.mainsnak
		if (snak.datatype == "wikibase-item")
			and (v.rank and mergedOpts.reqranks[v.rank:sub(1, 1)])
			and (snak.snaktype == "value")
			and (sourced(v) or not onlysrc) then
			out[#out+1] = snak.datavalue.value.id
		end
		if maxvals > 0 and #out >= maxvals then break end
	end
	
	return assembleoutput(out, mergedOpts, self.qid, pid)
end

function WdItem:getPropOfProp(prop1, prop2, opts)

	local mergedOpts = mergeOptions(self.options, opts)
	
	if not self.qid then return nil end
	
	local statements1 = self:getStatements(prop1, opts)
	if not statements1 or #statements1 == 0 then return nil end
	
	local onlysrc = parseParam(mergedOpts.onlysourced or mergedOpts.osd, true)
	local maxvals = tonumber(mergedOpts.maxvals) or 0
	local qualID = mw.text.trim(mergedOpts.qual or ""):upper()
	if qualID == "" then qualID = nil end
	
	local out = {}
	for k, v in ipairs(statements1) do
		if not onlysrc or sourced(v) then
			local snak = v.mainsnak
			if snak.datatype == "wikibase-item" and snak.snaktype == "value" then
				local qid2 = snak.datavalue.value.id
				local item2 = WdItem.new(qid2, mergedOpts)
				local statements2 = item2:getStatements(prop2, opts)
				if statements2 and statements2[1] then
					local out2 = propertyvalueandquals(statements2, mergedOpts, qualID)
					out[#out+1] = assembleoutput(out2, mergedOpts, qid2, prop2)
				end
			end
		end
		if maxvals > 0 and #out >= maxvals then break end
	end
	
	return assembleoutput(out, mergedOpts, self.qid, prop1)
end

function WdItem:getLangOfProp(pid)
	if not pid or not self.qid then return {} end
	local out = {}
	local props = mw.wikibase.getAllStatements(self.qid, pid)
	for _, v in ipairs(props) do
		if v.mainsnak.datatype == "monolingualtext" and v.mainsnak.datavalue then
			out[#out + 1] = v.mainsnak.datavalue.value.language
		end
	end
	return out
end

function WdItem:followQid(props, all)
	all = parseParam(all, false)
	props = (props or ""):upper()
	
	if not self.qid then return nil end
	if props == "" then return self.qid end
	
	local out = {}
	for p in mw.text.gsplit(props, "%p") do
		p = mw.text.trim(p)
		for i, v in ipairs(mw.wikibase.getBestStatements(self.qid, p)) do
			local linkedid = v.mainsnak.datavalue and v.mainsnak.datavalue.value.id
			if linkedid then
				if all then
					out[#out+1] = linkedid
				else
					return linkedid
				end
			end
		end
	end
	
	if #out > 0 then
		return table.concat(out, " ")
	else
		return self.qid
	end
end

-------------------------------------------------------------------------------
-- Public API - Utility Functions
-------------------------------------------------------------------------------

p.getStatementsByVol = function(qid, pid, vol, chapter)
	local statements = mw.wikibase.getBestStatements(qid, pid)
	if not statements then return nil end
	
	local filtered_statements = {}
	
	if chapter then
		for _, statement in ipairs(statements) do
			if statement.qualifiers and statement.qualifiers["P792"] then
				for _, qual in ipairs(statement.qualifiers["P792"]) do
					if qual.datavalue and tostring(qual.datavalue.value) == tostring(chapter) then
						table.insert(filtered_statements, statement)
						break
					end
				end
			end
		end
	end
	
	if #filtered_statements == 0 and vol then
		for _, statement in ipairs(statements) do
			local vol_match = true
			if vol and statement.qualifiers and statement.qualifiers["P478"] then
				vol_match = false
				for _, qual in ipairs(statement.qualifiers["P478"]) do
					if qual.datavalue and tostring(qual.datavalue.value) == tostring(vol) then
						vol_match = true
						break
					end
				end
			end
			if vol_match then
				table.insert(filtered_statements, statement)
			end
		end
	else
		filtered_statements = statements
	end
	
	return filtered_statements
end

-------------------------------------------------------------------------------
-- Public API - WdItem Constructors
-------------------------------------------------------------------------------

p.WdItem = WdItem

function p.newItem(qid, opts)
	return WdItem.new(qid, opts)
end

function p.currentItem(opts)
	return WdItem.new(nil, opts)
end

function p.newSnakItem(snakTable, opts)
	return WdItem.newFromSnaks(snakTable, opts)
end

return p