Module:Roadtable

From Wikidata
Jump to navigation Jump to search
Lua
CodeDiscussionLinksLink count SubpagesDocumentationTestsResultsSandboxLive code All modules


Code

local p = {}
local SANDBOX = false -- Change to false when deploying

-- Library aliases
local concat = table.concat
local format = mw.ustring.format
local insert = table.insert
local loadData = mw.loadData
local sort = table.sort
local split = mw.text.split
local strFormat = string.format
local trim = mw.text.trim

-- Data modules
local globalI18n = loadData('Module:Roadtable/i18n')
local modelsI18n = loadData('Module:Roadtable/models/i18n')
local sectionsI18n = loadData('Module:Roadtable/sections/i18n')
local modelData
if SANDBOX then
	modelData = loadData('Module:Roadtable/models/sandbox')
else
	modelData = loadData('Module:Roadtable/models')
end

-- Constants
local headerColor = '#eaecf0'

-- Global data
local fullModel = modelData.models.full
local globalData = {}
local sectionDatasets = {}
local importanceDatasets = {{}, {}, {}, {}}
local importanceData = {}
local perSectionData = {}
local root = ''
local fullPage

-- Translation code

local int_lang, subpage_lang

local function translate(str)
	return str[subpage_lang] or str[int_lang] or str.en
end

local importanceNumToStr, summaryStr

-- Utility functions

local function listParser(list)
	list = trim(list)
	local entries = split(list, ";")
	local fullEntries = {}
	for _,v in ipairs(entries) do
		local newEntry = split(v, '_')
		if #newEntry > 1 then
			insert(fullEntries, newEntry)
		else
			insert(fullEntries, v)
		end
	end

	return fullEntries
end

local function listToTruth(list)
	truth = {}
	for _,v in ipairs(list) do
		truth[v] = true
	end

	return truth
end

local function propTableToList(data)
	local properties = {}
	for _,property in ipairs(fullModel) do
		if not data.usedProperties[property] then
			insert(properties, -1)
		else
			insert(properties, data.properties[property])
		end
	end

	return properties
end

-- Header and footer

local function top(model)
	local headers = {'{| class=wikitable'}
	local itemHeader = translate(globalI18n.itemHeader)
	insert(headers, '!' .. itemHeader)
	local props
	if type(model) == 'table' then
		props = model
	else
		props = modelData.models[model]
	end
	local images = modelData.images
	local altStrs = modelsI18n.alt

	local langImage = images.langs
	local langAlt = translate(altStrs.langs)
	insert(headers, format('![[File:%s|25px|link=|%s]]', langImage, langAlt))

	for i,v in ipairs(props) do
		local image = images[v]
		if not image then
			insert(headers, strFormat('!%s', tostring(i)))
		else
			local alt = translate(altStrs[v])
			insert(headers, format('![[File:%s|25px|link=|%s]]', image, alt))
		end
	end
	if model ~= 'full' and SANDBOX then
		local classStr = translate(globalI18n.classes.header)
		insert(headers, '!' .. classStr)
	end

	return concat(headers, '\n')
end

local function bottom(model)
	local props
	if type(model) == 'table' then
		props = model
	else
		props = modelData.models[model]
	end
	local footers = {'|-'}
	local images = modelData.images
	local altStrs = modelsI18n.alt

	local langImage = images.langs
	local langAlt = translate(altStrs.langs)
	local langStr = translate(globalI18n.labelDesc)
	insert(footers, format('*[[File:%s|25px|link=|%s]] %s', langImage, langAlt, langStr))

	for i,v in ipairs(props) do
		local image = images[v]
		local entry
		if not image then
			entry = strFormat("*'''%s''' ", i)
		else
			local alt = translate(altStrs[v])
			entry = format('*[[File:%s|25px|link=|%s]] ', image, alt)
		end
		entry = entry .. mw.getCurrentFrame():preprocess(strFormat('{{P|%s}}', v))
		local additional = fullModel[v]
		if additional and type(additional) == 'table' then
			for _,v in ipairs(additional) do
				entry = entry .. mw.getCurrentFrame():preprocess(strFormat(', {{P|%s}}', v))
			end
		end
		insert(footers, entry)
	end
	local colspan = SANDBOX and (#footers + 1) or #footers
	insert(footers, 2, '| colspan="' .. tostring(colspan) .. '" class="hlist" style="text-align: center; background-color: ' .. headerColor .. '; font-size: 90%;"|')
	insert(footers, '|-\n|}')

	return concat(footers, '\n')
end

-- Languages

local function excludeLang(data, currLangs)
	local excludedLangs = {}
	for k,_ in pairs(data.languages) do
		if not currLangs[k] then
			excludedLangs[k] = true
		end
	end
	for k,_ in pairs(excludedLangs) do
		perSectionData.languages[k] = nil
		globalData.languages[k] = nil
		importanceData.languages[k] = nil
	end
end

local function languageList(langs)
	if not perSectionData.languages then
		perSectionData.languages = listToTruth(langs)
		if not globalData.languages then
			globalData.languages = listToTruth(langs)
		else
			if not importanceData.languages then
				importanceData.languages = listToTruth(langs)
			else
				excludeLang(importanceData, perSectionData.languages)
			end
			excludeLang(globalData, perSectionData.languages)
		end
		if not importanceData.languages then
			importanceData.languages = listToTruth(langs)
		else
			excludeLang(importanceData, perSectionData.languages)
		end
	else
		local currLangs = listToTruth(langs)
		excludeLang(perSectionData, currLangs)
	end

	return concat(langs, ' ')
end

local function autoLanguageList(item)
	local labels = item.labels or {}
	local descriptions = item.descriptions or {}
	local langs = {}
	for lang,_ in pairs(labels) do
		if descriptions[lang] then
			insert(langs, lang)
		end
	end
	sort(langs)

	return languageList(langs)
end

local function manualLanguageList(args)
	local langStr = args.langs or ''
	local langs = split(langStr, ',')
	sort(langs)

	return languageList(langs)
end

-- Properties

local fullModelProps = listToTruth(fullModel)
local dataProps = {__index=fullModelProps}

-- Class requirement parser
local classParser = {}
classParser['and'] = function (cells, requirements)
	for _,req in ipairs(requirements) do
		if type(req) == 'table' then
			if not classParser.parse(cells, req) then
				return false
			end
		else
			if not cells[req] then
				return false
			end
		end
	end
	return true
end
classParser['or'] = function (cells, requirements)
	for _,req in ipairs(requirements) do
		if type(req) == 'table' then
			if classParser.parse(cells, req) then
				return true
			end
		else
			if cells[req] then
				return true
			end
		end
	end
	return false
end
classParser['some'] = function (cells, requirements)
	local count = 0
	local needed = requirements.count
	for _,req in ipairs(requirements) do
		if type(req) == 'table' then
			if classParser.parse(cells, req) then
				count = count + 1
			end
		else
			if cells[req] then
				count = count + 1
			end
		end
	end
	return count >= needed
end
classParser.parse = function (cells, requirements)
	local operator = requirements.operator or 'and'
	return classParser[operator](cells, requirements)
end

local function modelPropertyRow(modelName, item)
	local models = modelData.models
	local model = models[modelName] or models.route
	if not perSectionData.properties then
		perSectionData.properties = {}
		setmetatable(perSectionData.properties, dataProps)
		perSectionData.usedProperties = {}
	end
	if not globalData.properties then
		globalData.properties = {}
		setmetatable(globalData.properties, dataProps)
		globalData.usedProperties = {}
	end
	if not importanceData.properties then
		importanceData.properties = {}
		setmetatable(importanceData.properties, dataProps)
		importanceData.usedProperties = {}
	end

	local modelUsed = listToTruth(model)

	local propertyList = item:getProperties()
	local properties = listToTruth(propertyList)

	local cells = {}
	local function presenceTester(value, property)
		local test
		if value ~= nil then
			if type(value) == 'table' then
				for _,v in ipairs(value) do
					if type(v) == 'number' then  -- check multiple allowed values
						test = test or presenceTester(v, property)
					elseif string.sub(v,1,1) == 'P' then -- check multiple allowed properties
						test = test or presenceTester(value[v], v)
					end
				end
			elseif type(value) == 'number' then
				local claims = item.claims or {}
				local claimsForProp = claims[property]
				if not claimsForProp then
					test = false
				else
					local itemId
					for _,claim in ipairs(claimsForProp) do
						itemId = claim.mainsnak.datavalue.value['numeric-id']
						test = (itemId == value)
						if test then
							break
						end
					end
				end
			elseif type(value) == 'boolean' then
				test = value
			end
		else
			test = (properties[property] == true)
		end
		return test
	end
	local function insertCell(property)
		if not modelUsed[property] then
			cells[property] = -1
		else
			perSectionData.usedProperties[property] = true
			globalData.usedProperties[property] = true
			importanceData.usedProperties[property] = true
			local value = model[property]
			local test = presenceTester(value, property)
			if not test then
				perSectionData.properties[property] = false
			end
			cells[property] = test
		end
	end

	for _,v in ipairs(fullModel) do
		insertCell(v)
	end

	if SANDBOX then
		local classes = modelData.classes[modelName]
		if classes then
			local letters = {'D', 'C', 'B', 'A'}
			local class = 'F'
			for _,v in ipairs(letters) do
				local requirements = classes[v]
				local met = classParser.parse(cells, requirements)
				if met then
					class = v
				else
					break
				end
			end
			cells.class = translate(globalI18n.classes[class])
		else
			cells.class = ''
		end
	end

	return cells
end

local function manualPropertyRow(args)
	if not perSectionData.properties then
		perSectionData.properties = {}
		setmetatable(perSectionData.properties, dataProps)
		perSectionData.usedProperties = {}
	end
	if not globalData.properties then
		globalData.properties = {}
		setmetatable(globalData.properties, dataProps)
		globalData.usedProperties = {}
	end
	if not importanceData.properties then
		importanceData.properties = {}
		setmetatable(importanceData.properties, dataProps)
		importanceData.usedProperties = {}
	end

	local propStrs = split(args.properties, ':')
	local properties = {}
	for _,v in pairs(propStrs) do
		local prop = split(v, '-')
		properties[prop[1]] = prop[2]
	end

	local cells = {class = ''}

	local function insertCell(property)
		local cases = {yes = true, no = false, na = -1}
		local test = cases[properties[property] or 'no']
		if test == -1 then
			cells[property] = -1
		else
			perSectionData.usedProperties[property] = true
			globalData.usedProperties[property] = true
			importanceData.usedProperties[property] = true
			if not test then
				perSectionData.properties[property] = false
			end
			cells[property] = test
		end
	end

	for _,v in ipairs(fullModel) do
		insertCell(v)
	end

	return cells
end

-- Row generation

local function manualHeader(args)
	local target, display

	-- Target
	local subpage = args.subpage or ''
	if subpage ~= '' then
		local rootPage = args.root or root
		local thisPage = "Wikidata:WikiProject Roads" .. rootPage
		target = format("Special:MyLanguage/%s/%s", thisPage, subpage)
	else
		target = args.link or ''
	end

	-- Display
	local q = args.targetq or ''
	local section = args.targetsection or ''
	if q ~= '' then -- Target item (use localized label for link display)
		local itemName = 'Q' .. q
		display = mw.wikibase.getLabelByLang(itemName, subpage_lang) or mw.wikibase.label(itemName)
	elseif section ~= '' then
		local temp = split(section, '-')
		if #temp > 1 then
			section = concat(temp, '_')
		end
		local translations = sectionsI18n[section] or {en = 'Section ' .. section}
		display = translate(translations)
	else
		display = args.target or ''
	end

	local link = format("[[%s|%s]]", target, display)
	return link
end

local function getData(args)
	local q = args.q or ''
	local model = args.model or ''
	local item, langs, properties, header

	item = mw.wikibase.getEntityObject('Q' .. q)
	if not item then
		return {noitem='Q' .. q}
	end

	langs = autoLanguageList(item)
	properties = modelPropertyRow(model, item)
	local label = item:getLabel(subpage_lang) or item:getLabel(int_lang) or item:getLabel('en')
	if not label then
		header = format("[[Q%s]]", q)
	else
		header = format("[[Q%s|%s <small>(Q%s)</small>]]", q, label, q)
	end

	return {header=header, langs=langs, props=properties}
end

local function rowGen(args, summary)
	local row = {}

	if not summary then
		local data = getData(args)
		if data.noitem then
			return data
		end
		row.def = '|-'
		row.header = data.header
		row.langs = data.langs
		row.properties = data.props
	elseif summary == -1 then
		row.def = '|-'
		row.header = manualHeader(args)
		row.langs = manualLanguageList(args)
		row.properties = manualPropertyRow(args)
	else
		row.def = '|- style="background-color: ' .. headerColor .. '; border-top: 2px solid black !important;" | '
		row.header = summaryStr

		local languages = {}
		for k,_ in pairs(perSectionData.languages) do
			insert(languages, k)
		end
		sort(languages)
		row.langs = concat(languages, '&nbsp;')

		local properties = {}
		for _,property in ipairs(fullModel) do
			if not perSectionData.usedProperties[property] then
				properties[property] = -1
			else
				properties[property] = perSectionData.properties[property]
			end
		end
		properties.class = ''
		row.properties = properties
	end

	return row
end

local function overviewRowGen(data, summary)
	local row
	local langs, properties
	if not summary then
		local importanceData = importanceDatasets[data.importance]
		row = {'|-'}
		insert(row, format('! [[#%s|%s]]', data.name,  data.label))
		if not data.placeholder then
			local languages = {}
			for k,_ in pairs(data.languages) do
				insert(languages, k)
			end
			sort(languages)
			langs = concat(languages, '&nbsp;')

			properties = propTableToList(data)
			for _,property in ipairs(fullModel) do
				if not data.properties[property] then
					globalData.properties[property] = false
					importanceData.properties[property] = false
				end
			end
		else
			insert(row, "|style=\"text-align: center;\" colspan=15|''No data''")
			return concat(row, '\n')
		end
	elseif type(summary) == 'number' then
		if not data.languages then return nil end
		row = {'|- style="background-color: ' .. headerColor .. ';" | '}
		insert(row, format('! %s', importanceNumToStr[summary]))
		local languages = {}
		for k,_ in pairs(data.languages) do
			insert(languages, k)
		end
		sort(languages)
		langs = concat(languages, '&nbsp;')

		properties = propTableToList(data)
	else
		row = {'|- style="background-color: ' .. headerColor .. '; border-top: 2px solid black !important;" | '}
		insert(row, '! ' .. summaryStr)
		local languages = {}
		for k,_ in pairs(globalData.languages) do
			insert(languages, k)
		end
		sort(languages)
		langs = concat(languages, '&nbsp;')

		properties = propTableToList(globalData)
	end

	insert(row, '|' .. langs)
	for _,prop in ipairs(properties) do
		if prop == -1 then
			insert(row, '| style="text-align: center; background-color: #d3d3d3;" | &mdash;')
		elseif prop then
			insert(row, '| style="text-align: center; background-color: #ddffdd;" | ✓')
		else
			insert(row, '| style="text-align: center; background-color: #ffdddd;" | ✗')
		end
	end

	return concat(row, '\n')
end

-- Tables

local function body(args, section, level)
	local sectionDataset
	if SANDBOX then
		sectionDataset = loadData('Module:Roadtable/sections/sandbox')
	else
		sectionDataset = loadData('Module:Roadtable/sections')
	end
	local sectionData = sectionDataset[section]
	local model = args[section .. '_model'] or ''
	local importance = tonumber(args[section .. '_importance'])
	if sectionData then
		model = model == '' and sectionData.model or model
		importance = importance or sectionData.importance
	end
	importanceData = importanceDatasets[importance]
	perSectionData = {name = section, importance = importance}

	local rowData = {}
	local rows = {}
	local used = {}

	local entryList = args[section] or ''
	if entryList ~= '' then
		local entries = listParser(entryList)

		for i,v in ipairs(entries) do
			local rowArgs = {}
			local q
			if type(v) == 'table' then
				q = v[1]
			else
				q = v
			end
			if q == '' or q == 'noitem' then -- No item (use string for name)
				local noitem = v[2]
				if not noitem then
					error(strFormat('No title provided for noitem at position %s', i), 0)
				else
					local noitemTitle = split(noitem, '=')[2]
					insert(rowData, {noitem=noitemTitle})
				end
			elseif q == 'manual' then
				for idx,arg in ipairs(v) do
					if idx > 1 then
						local param = split(arg, '=')
						rowArgs[param[1]] = param[2]
					end
				end
				insert(rowData, rowGen(rowArgs, -1))
			else
				rowArgs.q = trim(q)
				if type(v) == 'table' then
					rowArgs.model = split(v[2], '=')[2]
				else
					rowArgs.model = model
				end
				insert(rowData, rowGen(rowArgs))
			end
		end
		insert(rowData, rowGen({}, true))

		for _,v in ipairs(fullModel) do
			if perSectionData.usedProperties[v] then
				insert(used, v)
			end
		end
		insert(rows, top(used))
		for _,row in ipairs(rowData) do
			if row.noitem then
				insert(rows, format("|-\n!%s\n|colspan=15 style=\"text-align: center;\"|''Item not created''", row.noitem))
			else
				local rowStrs = {row.def, '! ' .. row.header, '| ' .. row.langs}
				props = row.properties
				for _,prop in ipairs(used) do
					if props[prop] == -1 then
						insert(rowStrs, '| style="text-align: center; background-color: #d3d3d3;" | &mdash;')
					elseif props[prop] then
						insert(rowStrs, '| style="text-align: center; background-color: #ddffdd;" | ✓')
					else
						insert(rowStrs, '| style="text-align: center; background-color: #ffdddd;" | ✗')
					end
				end
				if SANDBOX then insert(rowStrs, '| style="text-align: center;" | ' .. row.properties.class) end
				insert(rows, concat(rowStrs, '\n'))
			end
		end
	else
		perSectionData.placeholder = true
	end

	local anchor = strFormat('<span id="%s"></span>', section)
	local header
	if sectionData then
		local translations = sectionsI18n[section] or {en = 'Section ' .. section}
		header = translate(translations)
	else
		local title = args[section .. '_title'] or tonumber(args[section .. '_titleq'] or '') or ''
		if title == '' then
			header = 'Section ' .. section
		elseif type(title) == 'number' then
			local itemName = 'Q' .. title
			header = mw.wikibase.getLabelByLang(itemName, subpage_lang) or mw.wikibase.label(itemName)
		else
			header = title
		end
	end
	if level == 2 then
		local headerWikitext
		if fullPage then
			headerWikitext = format('===%s %s===', anchor, header)
		else
			headerWikitext = format('====%s %s====', anchor, header)
		end
		insert(rows, 1, headerWikitext)
		perSectionData.label = '— ' .. header
	else
		local headerWikitext
		if fullPage then
			headerWikitext = format('==%s %s==', anchor, header)
		else
			headerWikitext = format('===%s %s===', anchor, header)
		end
		insert(rows, 1, headerWikitext)
		perSectionData.label = header
	end
	if entryList ~= '' then
		insert(rows, bottom(used))
	end
	insert(sectionDatasets, perSectionData)

	return concat(rows, '\n')
end

local function overview()
	local rows = {}

	for i,v in ipairs(sectionDatasets) do
		insert(rows, overviewRowGen(v))
	end
	insert(rows, overviewRowGen({}, true))
	for i = 4, 1, -1 do
		insert(rows, overviewRowGen(importanceDatasets[i], i))
	end

	insert(rows, 1, top('full'))
	insert(rows, bottom('full'))

	return concat(rows, '\n')
end

-- Pages

function p._page(args)
	local sections = {}
	local sectionList = args.sections
	root = args.root or ''
	fullPage = (args.fullpage ~= 'no')
	local sectionsTable = listParser(sectionList)
	for _,section in ipairs(sectionsTable) do
		if type(section) == 'table' then
			for _,subsection in ipairs(section) do
				if subsection == section[1] then
					insert(sections, body(args, subsection, 1))
				else
					insert(sections, body(args, section[1] .. '_' .. subsection, 2))
				end
			end
		else
			insert(sections, body(args, section))
		end
	end
	insert(sections, 1, '__NOTOC__')
	insert(sections, 1, overview())

	return concat(sections, '\n\n')
end

function p.page(frame)
	local pframe = frame:getParent()
	local config = frame.args -- the arguments passed BY the template, in the wikitext of the template itself
	local args = pframe.args -- the arguments passed TO the template, in the wikitext that transcludes the template

	--int_lang = frame:preprocess('{{int:Lang}}')
	int_lang = config.lang
	subpage_lang = config.lang
	importanceNumToStr = translate(globalI18n.importance)
	summaryStr = translate(globalI18n.summary)
	return p._page(args)
end

return p