A modult a Modul:Wikidata/doc lapon tudod dokumentálni

require"strict"

local p = {}

local getArgs = require"Modul:Arguments".getArgs
local frame = mw.getCurrentFrame()

local i18n = {
    ["errors"] = {
        ["property-param-not-provided"] = "Hiányzó ''property='' paraméter",
        ["entity-not-found"] = "Nem létező Wikidata-elem",
        ["unknown-claim-type"] = "Ismeretlen az állítás típusa",
        ["unknown-snak-type"] = "Ismeretlen a snak típusa",
        ["unknown-datavalue-type"] = "Ismeretlen az érték típusa",
        ["unknown-entity-type"] = "Ismeretlen a Wikidata-elem típusa",
        ["unknown-value-module"] = "A ''value-module'' és ''value-function'' paramétert egyszerre kell beállítani",
        ["value-module-not-found"] = "A ''value-module'' nem létező modulra mutat",
        ["value-function-not-found"] = "A ''value-function'' nem létező funkcióra mutat",
        ["globecoordinate-direction"] = "Az érték típusa ''globecoordinate'' : kell ''direction=latitude'' vagy ''longitude''",
        ["invalid-value"] = "Érvénytelen érték",
        ["unknown-unit"] = "Ismeretlen mértékegység",
    },
    ["somevalue"] = "''nem ismert''",
    ["novalue"] = "''nincs''"
}

local function formatError(key)
	error(i18n.errors[key], 2)
end

local function getUpperLevelOfType(property, typeId, item)
	local result, statements, visited = {}, {}, {[item.id] = true}
	local function getStatements(item)
		if not item or not item.claims or not item.claims[property] then
			return
		end
		for _, s in ipairs(item:getBestStatements(property)) do
			if s.mainsnak.snaktype == "value" then
				local itemId = "Q" .. s.mainsnak.datavalue.value["numeric-id"]
				if not visited[itemId] then
					visited[itemId] = true
					local item = mw.wikibase.getEntity(itemId)
					if p.containsPropertyWithValue(item, "P31", typeId) then
						statements[item.id] = s
					else
						getStatements(item)
					end
				end
			end
		end
	end
	getStatements(item)
	for _, s in pairs(statements) do
		table.insert(result, s)
	end
	return result
end

local function firstValue(statements)
	for _, statement in ipairs(statements) do
		if statement.rank == 'preferred' then
			return {statement}
		end
	end
	for _, statement in ipairs(statements) do
		if statement.rank == 'normal' then
			return {statement}
		end
	end
	return {}
end

local function withRank(statements, ranks)
	local result = {}
	for _i, statement in ipairs(statements) do
		for _j, rank in ipairs(ranks) do
			if statement.rank == rank then
				table.insert(result, statement)
				break
			end
		end
	end
	return result
end

function p.withHighestRank(statements)
	local preferred, normal = {}, {}
	for _, statement in ipairs(statements) do
		if statement.rank == "preferred" then
			table.insert(preferred, statement)
		elseif statement.rank == "normal" then
			table.insert(normal, statement)
		end
	end
	return #preferred > 0 and preferred or normal
end

local function atDate(statements, date)
	local result = {}
	local Time = require"Modul:Time"
	local time = Time.newFromIso8601(date, true)
	if not time then
		return statements
	end
	for _, s in ipairs(statements) do
		local startDate, endDate
		if s.qualifiers and s.qualifiers.P580 and #s.qualifiers.P580 == 1 and s.qualifiers.P580[1].snaktype == "value" then
			startDate = Time.newFromWikidataValue(s.qualifiers.P580[1].datavalue.value)
		end
		if s.qualifiers and s.qualifiers.P582 and #s.qualifiers.P582 == 1 and s.qualifiers.P582[1].snaktype == "value" then
			endDate = Time.newFromWikidataValue(s.qualifiers.P582[1].datavalue.value)
		end
		if not startDate and endDate and time <= endDate or
			startDate and not endDate and startDate <= time or
			startDate and endDate and startDate <= time and time <= endDate then
			table.insert(result, s)
		end
	end
	return result
end

local function getEntityFromId(id)
    if id and id ~= '' then
        return mw.wikibase.getEntity(id)
    end
    return mw.wikibase.getEntity()
end

local function getEntityIdFromValue(value)
    local prefix
    if value['entity-type'] == 'item' then
        prefix = 'Q'
    elseif value['entity-type'] == 'property' then
        prefix = 'P'
    else
        formatError"unknown-entity-type"
    end
    return prefix .. value['numeric-id']
end

local function populationWithPointInTime(statement, options)
	local population = tonumber(statement.mainsnak.datavalue.value.amount)
	local text = (population < 10000 and tostring(population) or mw.getContentLanguage():formatNum(population)) .. ' fő'

	if statement.qualifiers and statement.qualifiers.P585 then  -- dátum
		local time, fDate = require'Modul:Time'.newFromWikidataValue(statement.qualifiers.P585[1].datavalue.value)
		if time.precision >= 11 then
			fDate = mw.getContentLanguage():formatDate('Y. M. j.', time:toIso8601())
		else
			fDate = tostring(time.year)
		end
		text = text .. ' ' .. mw.text.tag('span', {style = 'font-size:90%; white-space:nowrap;'}, '(' .. fDate .. ')')
	end
	return text
end

local function formatEntityId(entityId, options)
	local link = mw.wikibase.sitelink(entityId)
	if options.link == 'csak' then
		return link
	end
	if link and options.link ~= 'nem' and mw.ustring.sub(link, 1, 10) == 'Kategória:' then
		return '[[' .. link .. ']]'
	end
	local label = mw.wikibase.label(entityId)
	if options.labelProperty then
		local options2 = {}
		for k, v in pairs(options) do
			if k ~= "labelProperty" then
				options2[k] = v
			end
		end
		options2.property = options.labelProperty
		options2.entityId = entityId
		options2.rank = "valid"
		options2.link = "nem"
		options2.lang = options.lang or "hu"
		options2.firstAfter = true
		options2['felsorolás'] = nil -- felsorolásjel nélkül
		local label2 = p.formatStatements(frame, options2)
		if label2 and label2 ~= "" then
			label = label2
		end
	end
	if options.chrProperty and options.chrDate then
		local time
		if options.chrDate:match"^P%d+$" then
			local dates = mw.wikibase.getEntity(options.entityId):getBestStatements(options.chrDate)
			if #dates == 1 and dates[1].mainsnak.snaktype == "value" then
				time = dates[1].mainsnak.datavalue.value.time
			end
		else
			time = options.chrDate
		end
		if time then
			local chrLabel = p.formatStatements(frame, {
				property = options.chrProperty,
				entityId = entityId,
				atDate = time,
				rank = "valid",
				lang = options.lang or "hu",
				firstAfter = true,
				['felsorolás'] = nil -- felsorolásjel nélkül
			})
			if chrLabel and chrLabel ~= "" then
				label = chrLabel
			end
		end
	end
	if label and options.format == "ucfirst" then
		label = mw.language.getContentLanguage():ucfirst(label)
	end
	if link and options.link ~= 'nem' then
		if label then
			if mw.ustring.sub(label, 2) == mw.ustring.sub(link, 2) and 
				mw.ustring.lower(mw.ustring.sub(label, 1, 1)) == mw.ustring.lower(mw.ustring.sub(link, 1, 1)) then
				return '[[' .. label .. ']]'
			else
				return '[[' .. link .. '|' .. label .. ']]'
			end
		else
			return '[[' .. link .. ']]'
		end
	else
		return label or link  --TODO what if no links and label + fallback language?
	end
end

local function formatTimeValue(value, options)
    if options.format == "raw" then
        return value.time
    else
        local time = require"Modul:Time".newFromWikidataValue(value)
        if time then
			if options.format == "iso" then
				return tostring(time)
			end
            return time:formatDate(options)
        else
            formatError"invalid-value"
        end
    end
end

local function countryOf(itemId)
	if not itemId then
		return nil
	end
	local item = mw.wikibase.getEntity(itemId)
	if not item then
		return nil
	end
	return p.formatStatements(frame, {property = "P17"}, item)
end

local function formatNum(amount)
	if amount < 10000 and -10000 < amount then
		return tostring(amount):gsub('%.', ',')
	else
		return mw.getContentLanguage():formatNum(amount)
	end
end

local function formatDatavalue(datavalue, options)
    --Use the customize handler if provided
    if options['value-module'] or options['value-function'] then
        if not options['value-module'] or not options['value-function'] then
            return formatError( 'unknown-value-module' )
        end
        local formatter = require ('Module:' .. options['value-module'])
        if formatter == nil then
            return formatError( 'value-module-not-found' )
        end
        local fun = formatter[options['value-function']]
        if fun == nil then
            return formatError( 'value-function-not-found' )
        end
        return fun( datavalue.value, options )
    end

    --Default formatters
    if datavalue.type == 'wikibase-entityid' then
		local itemId = getEntityIdFromValue(datavalue.value)
		if options.format == 'raw' then
			return itemId
		end
		local result = formatEntityId(itemId, options)
		if not result then
			return nil
		end
		local country = options.format == "with_country" and countryOf(itemId, options)
        return result .. (country and ", " .. country or "")
    elseif datavalue.type == 'string' then
        return datavalue.value --TODO ids + media
    elseif datavalue.type == 'time' then
        return formatTimeValue(datavalue.value, options)
    elseif datavalue.type == 'globecoordinate' then
        if options.direction == 'latitude' then
            return datavalue.value.latitude
        elseif options.direction == 'longitude' then
            return datavalue.value.longitude
        else
            return formatError('globecoordinate-direction')
        end
    elseif datavalue.type == 'quantity' then
		if options.format == 'raw' then
			return datavalue.value.amount
		end
		local result
		local amount = tonumber(datavalue.value.amount)
		if datavalue.value.unit == "1" then
			if options.unit then
				return nil
			end
			result = formatNum(amount)
		else
			local unitId = datavalue.value.unit:match"Q%d+"
			local sourceUnit = mw.loadData"Modul:Wikidata/units".wikidata_item_ids[unitId]
			if not sourceUnit then
				if not options.unit then
					local unitItem = mw.wikibase.getEntity(unitId)
					if p.isOfType(unitItem, "Q8142") then  -- currency
						local symbol = unitItem:getBestStatements("P558")[1]
						result = formatNum(amount) .. " " .. (symbol and p.formatStatement(symbol) or unitItem:getLabel())
					else
						result = formatNum(amount) .. " " .. unitItem:getLabel()
					end
				else
					formatError"unknown-unit"
				end
			else
				local targetUnit = options.unit or sourceUnit
				result = frame:expandTemplate{title = "Convert", args = {amount, sourceUnit, targetUnit, disp = options.showUnit and 
					"out" or "number"
				}}
			end
		end
		return result
	elseif datavalue.type == "monolingualtext" then
		if not options.lang or options.lang == "~hu" and datavalue.value.language ~= "hu" then
			return datavalue.value.text
		end
		for lang in mw.text.gsplit(options.lang, ",") do
			if lang:match"^%s*(.-)%s*$" == datavalue.value.language then
				return datavalue.value.text
			end
		end
		return nil
	else
		formatError"unknown-datavalue-type"
	end
end

local function formatSnak(snak, options)
	local options = options or {}
	if snak.snaktype == "somevalue" then
		return options.somevalue or i18n["somevalue"]
	elseif snak.snaktype == "novalue" then
		return i18n["novalue"]
	elseif snak.snaktype == "value" then
		if options["value-module"] or options["value-function"] then
			return formatDatavalue(snak.datavalue, options)
		end
		if snak.datatype == "math" then
			return frame:extensionTag("math", snak.datavalue.value)
		end
		if snak.datatype == "external-id" then
			if options['formatExternal'] then
				local formatter = p.formatStatements{
					entityId = snak.property,
					property = 'P1630',
					first = true
				}
				if formatter and formatter ~= '' then
					local id = formatDatavalue(snak.datavalue, options)
					local url = string.gsub( formatter, '%$1', id )
					return string.format( '[%s %s]', url, id )
				end
				-- else fall back to plain version
			end
			return formatDatavalue(snak.datavalue, options)
		end
		return formatDatavalue(snak.datavalue, options)
	else
		formatError"unknown-snak-type"
	end
end

local function formatSnaks(snaks, options)
	local formattedSnaks = {}
	for _, snak in ipairs(snaks) do
		table.insert(formattedSnaks, formatSnak(snak, options))
	end
	return mw.text.listToText(formattedSnaks, options.separator, options.conjunction)
end

local function formatReference(reference, options)
	if reference.snaks.P854 and (reference.snaks.P357 or reference.snaks.P1476) then  -- url and title
		local args, Time = {}
		if reference.snaks.P577 or reference.snaks.P813 then
			Time = require'Modul:Time'
		end

		args.url = reference.snaks['P854'] and formatSnak(reference.snaks['P854'][1], options)
		args.title = reference.snaks['P357'] and formatSnak(reference.snaks['P357'][1], options)
		args.author = reference.snaks['P50'] and formatSnak(reference.snaks['P50'][1], options)
		if reference.snaks.P577 then
			local time = Time.newFromWikidataValue(reference.snaks.P577[1].datavalue.value)
			if time.precision >= 11 then
				args.date = time:toIso8601()
			else
				args.year = tostring(time.year)
			end
		end
		args.publisher = reference.snaks['P123'] and formatSnak(reference.snaks['P123'][1], options)
		args.language = reference.snaks['P364'] and formatSnaks(reference.snaks['P364'], {link = 'nem'})
		args.accessdate = reference.snaks['P813'] and Time.newFromWikidataValue(reference.snaks.P813[1].datavalue.value):toIso8601()

		if args.url and args.url:sub(1, 17) == 'http://www.ksh.hu' then
			args.url = args.url:gsub('p_lang=EN', 'p_lang=HU')
		end

		if not args.title and reference.snaks.P1476 then
			for _, snak in ipairs(reference.snaks.P1476) do
				if not args.title or (snak.datavalue and snak.datavalue.value.language == 'hu') then
					args.title = snak.datavalue.value.text
				end
			end
		end

		return frame:expandTemplate{title = 'Cite web', args = args}
	else
		local result = {}
		options.formatExternal = true
		for key, referenceSnaks in pairs(reference.snaks) do
			if key ~= 'P143' then  -- imported from
				for _, snak in ipairs(referenceSnaks) do
					table.insert(result, formatSnak(snak, snak.datavalue and snak.datavalue.type == 'time' and {link = 'nem'} or options))
				end
			end
		end
		return table.concat(result, ', ')
	end
end

local function formatStatements(options, item)
	if not options.property then
		formatError"property-param-not-provided"
	end
	local property = options.property:upper()
	--Get entity
	local entity = item
	if not entity then
		entity = getEntityFromId(options.entityId)
	end
    if not entity then
        return ''  --TODO error?
    end
    if not entity.claims or not entity.claims[property] then
        return ''  --TODO error?
    end
	local statements = entity.claims[property]

	-- TODO Extract selection and filtering
	if options.rank ~= 'all' then
		if not options.rank then
			statements = p.withHighestRank(statements)
		elseif options.rank == 'valid' then
			statements = withRank(statements, {'normal', 'preferred'})
		else
			statements = withRank(statements, {options.rank})
		end
	end

	if options.typeId then
		statements = getUpperLevelOfType(property, options.typeId, entity)
	end

	if options.atDate then
		statements = atDate(statements, options.atDate)
	end

	if options.excludespecial then
		local newStatements = {}
		for _, s in ipairs(statements) do
			if s.mainsnak.snaktype == "value" then
				table.insert(newStatements, s)
			end
		end
		statements = newStatements
	end

	if options.first then
		statements = firstValue(statements)
	end

	-- TODO Extract sorting
	if options.sort then
		local comp = options.sort
		if comp == "" then
			comp = function (s1, s2)
				if s1.mainsnak.snaktype ~= "value" or s2.mainsnak.snaktype ~= "value" then
					return nil
				end
				local sortKey
				local type = s1.mainsnak.datavalue.type
				if type == "wikibase-entityid" then
					sortKey = function(statement)
						local id = "Q" .. statement.mainsnak.datavalue.value["numeric-id"]
						local key = mw.wikibase.label(id)
						if not key then
							key = mw.wikibase.sitelink(id)
						end
						if not key then
							return id
						end
						return mw.language.getContentLanguage():caseFold(key)
					end
				end
				return sortKey(s1) < sortKey(s2)
			end
		end
		table.sort(statements, comp)
	end

    --Format statement and concat them cleanly
    local formattedStatements = {}
    for _, statement in ipairs(statements) do
        local fs
        if property == 'P1082' and options.format == 'default' then  -- population
			fs = populationWithPointInTime(statement, options)
			if statement.references then
				for _, reference in ipairs(statement.references) do
					local formattedReference = formatReference(reference, options)
					if formattedReference and formattedReference ~= '' then
						fs = fs .. frame:extensionTag('ref', formattedReference, {name = reference.hash})
					end
				end
			end
        else
            fs = p.formatStatement(statement, options)
        end
        if fs then
            if options['felsorolás'] == 'lista' then
                fs = '* ' .. fs
            elseif options['felsorolás'] == 'számozott lista' then
                fs = '# ' .. fs
            end
            table.insert(formattedStatements, fs)
        end
    end

	local function plainlist(items)
		if #items == 0 then
			return ""
		end
		if #items == 1 then
			return items[1]
		end
		return frame:expandTemplate{ title = "Plainlist", args = { "\n* " .. table.concat(items, "\n* ") .. "\n" } }
	end

    if options['felsorolás'] == 'lista' or options['felsorolás'] == 'számozott lista' then
        return table.concat(formattedStatements, '\n')
    elseif options['felsorolás'] == 'sorok' then
		return plainlist(formattedStatements)
    elseif options['felsorolás'] == 'szöveg' then
        return mw.text.listToText(formattedStatements)
    elseif options.separator or options.conjunction then
        options.separator = options.separator and string.gsub(options.separator, '&#32;', ' ')
        options.conjunction = options.conjunction and string.gsub(options.conjunction, '&#32;', ' ')
        return mw.text.listToText(formattedStatements, options.separator, options.conjunction)
    else
		if options.firstAfter then
			return formattedStatements[1] or ""
		end
		return plainlist(formattedStatements)
    end
end

local function formatQualifiers(statement, options)
	local result, startDate, endDate = {}
	for key, snaks in pairs(statement.qualifiers) do
		if key == 'P580' then
			startDate = formatSnak(snaks[1], {link = 'nem'})
		elseif key == 'P582' then
			endDate = formatSnak(snaks[1], {link = 'nem'})
		else
			for _, snak in ipairs(snaks) do
				table.insert(result, formatSnak(snak, {link = 'nem'}))
			end
		end
	end
	if startDate and startDate ~= '' or endDate and endDate ~= '' then
		table.insert(result, 1, (startDate or '') .. '–' .. (endDate or ''))
	end
	return table.concat(result, ', ')
end

function p.formatStatement(statement, options)
	if not statement.type or statement.type ~= 'statement' then
		formatError('unknown-claim-type')
	end
	local options = options or {}
	local result
	if statement.mainsnak.snaktype == "somevalue" and statement.mainsnak.datatype == "time" and statement.qualifiers and 
		(statement.qualifiers.P1319 or statement.qualifiers.P1326) then
		-- TODO Extract method
		if statement.qualifiers.P1319 then
			if statement.qualifiers.P1326 then
				result = formatSnak(statement.qualifiers.P1319[1]) .. " és " .. formatSnak(statement.qualifiers.P1326[1]) .. " között"
			else
				result = formatSnak(statement.qualifiers.P1319[1]) .. " után"
			end
		else
			result = formatSnak(statement.qualifiers.P1326[1]) .. " előtt"
		end
	else
		result = formatSnak(statement.mainsnak, options)
	end

	--TODO reference and qualifiers
	if result and result ~= '' then
		if options.showQualifiers and statement.qualifiers then
			local formattedQualifiers = formatQualifiers(statement, options)
			if formattedQualifiers and formattedQualifiers ~= '' then
				result = result .. ' <small>(' .. formattedQualifiers .. ')</small>'
			end
		end
		if options.showReferences and statement.references then
			for _, reference in ipairs(statement.references) do
				local formattedReference = formatReference(reference, options)
				if formattedReference and formattedReference ~= '' then
					result = result .. frame:extensionTag('ref', formattedReference, {name = reference.hash})
				end
			end
		end
	end
	return result
end

function p.formatStatements(frame, args, item)
	if not args then
		args = getArgs(frame, { removeBlanks = false })
	end

	--If a value if already set, use it
	if args.value and args.value ~= '' then
		return args.value ~= '-' and args.value or ''
	end
	return formatStatements(args, item)
end

--[[
	Returns string true if connected Wikibase item contains property specified 
	by property argument, empty string otherwise.
	Used by template Wikidata-f in conditional expressions.
--]]
function p.containsProperty(frame, args, item)
	if not args then
		args = getArgs(frame, {frameOnly = true, readOnly = true})
	end
	if args.value == '-' then
		return ''
	end
	if args.value then
		return 'true'
	end
	if not args.property then
		formatError('property-param-not-provided')
	end
	local entity = item or mw.wikibase.getEntity(args.entityId)
	if not entity or not entity.claims or not entity.claims[args.property:upper()] then
		return ''
	end
	return 'true'
end

function p.containsPropertyWithValue(item, property, value)
	if not property or not value then
		return false
	end
	if not item or not item.claims or not item.claims[property:upper()] then
		return false
	end
	for _, statement in ipairs(item.claims[property:upper()]) do
		if statement.rank ~= "deprecated" and statement.mainsnak.snaktype == "value" then
			local type = statement.mainsnak.datavalue.type
			if type == "wikibase-entityid" then
				if "Q" .. statement.mainsnak.datavalue.value["numeric-id"] == value then
					return true
				end
			end
		end
	end
	return false
end

function p.isOfType(item, class)
	if not item or not item.claims or not item.claims.P31 or not class then
		return false
	end
	local isSubclass, visited

	local function checkProperty(item, property)
		for _, s in ipairs(item:getBestStatements(property)) do
			if s.mainsnak.snaktype == "value" then
				local itemId = "Q" .. s.mainsnak.datavalue.value["numeric-id"]
				if itemId == class or isSubclass(itemId) then
					return true
				end
			end
		end
		return false
	end

	isSubclass = function (itemId)
		if visited[itemId] then
			return false
		end
		local item = mw.wikibase.getEntity(itemId)
		if not item then  -- deleted item
			return false
		end
		visited[itemId] = true
		visited[item.id] = true
		return checkProperty(item, "P279")
	end

	visited =  { [item.id] = true }
	return checkProperty(item, "P31")
end

return p