Module:Routelist row/sandbox 2

From Wikipedia, the free encyclopedia

local p = {} -- Package to be exported

-- Change to "" upon deployment.
local moduleSuffix = ""

local lang = mw.getContentLanguage() -- Built-in locale for date formatting
local format = mw.ustring.format -- String formatting function
local insert = table.insert
local concat = table.concat
local util = require("Module:Road data/util")
local frame = mw.getCurrentFrame()

local parserModuleName = "Module:Road data/parser" .. moduleSuffix
local statenameModuleName = "Module:Jct/statename" .. moduleSuffix -- TODO transition

local concat = table.concat
local insert = table.insert
local format = mw.ustring.format
local trim = mw.text.trim

local parserModule = require(parserModuleName)
local parser = parserModule.parser

-- Shields
local defaultShieldSize = 28

local function addContextBanner(args, name, suffix, bannerSpec)
	local bannerModule = 'Module:Road data/banners/' .. string.upper(args.country)
	local shieldfield = name .. 'shield'
	local shield = parser(args, shieldfield)
	if shield == nil then
		-- This route type does not define shield.
		-- Find shield in the default banner table.
		shield = parser(args, 'shield', name, bannerModule)
		if shield and shield ~= '' then
			if suffix == nil then
				suffix = parser(args, 'shield', 'suffix', bannerModule)
			end
			if suffix and suffix ~= '' then
				shield = shield .. " " .. suffix
			end
			shield = shield .. ".svg"
		end
	end
	if shield and shield ~= '' then
		local shieldSize = defaultShieldSize
		-- Add banner plate.
		insert(bannerSpec, {shield, shieldSize})
	end
end

local function bannerSpec(banner, bannerSize, bannerSuffix, route)
	local banners = {}
	if type(banner) == "table" then
		local bannerSizeIsNotTable = type(bannerSize) ~= "table"
		for i,filename in ipairs(banner) do
			local bannersize = bannerSizeIsNotTable and bannerSize or bannerSize[i] or defaultShieldSize
			insert(banners, {filename, bannersize})
		end
	elseif banner ~= '' then
		insert(banners, {banner, bannerSize})
	end

	return banners
end

local function shieldSpec(args, mainShield, shieldList)
	local shieldSpec = {}

	local shield

	if not shield then shield = parser(args, 'shieldlist') or parser(args, 'shield') or '' end
	
	if shield == '' then return shieldSpec end
	
	local orientation = parser(args, 'orientation')

	local function size(args)
		if orientation == "upright" then
			return defaultShieldSize
			else return "x" .. defaultShieldSize
		end
	end
	
	local shieldsize = size(args)
	
	local banner = parser(args, 'banner') or {}
	local bannersize = defaultShieldSize
	local bannersuffix = parser(args, 'bannersuffix')

	local bannerIsNotTable = type(banner) ~= "table"
	local bannersizeIsNotTable = type(bannersize) ~= "table"
	local bannersuffixIsNotTable = type(bannersuffix) ~= "table"

	if type(shield) == "table" then
		for i,filename in ipairs(shield) do
			local size = shieldsize[i] or shieldsize
			if size == "" then size = nil end
			-- banner.all describes banners that apply to all multiple shields.
			local shieldBanner = bannerIsNotTable and banner or (banner[i] or banner.all or {})
			-- Banner size is default if the corresponding entry
			-- in bannerSize table is not set.
			local shieldBannerSize =
				bannersizeIsNotTable and bannersize
				or (bannersize[i] or bannersize.all or defaultShieldSize)
			local shieldBannerSuffix = bannersuffix and (bannersuffixIsNotTable and bannersuffix or bannersuffix[i])
			insert(shieldSpec, {
				shield = {filename, size},
				banners = bannerSpec(shieldBanner, shieldBannerSize, shieldBannerSuffix, route)
			})
		end
	elseif shield ~= '' then
		if shieldsize == "" then shieldsize = nil end
		insert(shieldSpec, {
			shield = {shield, shieldsize},
			banners = bannerSpec(banner, bannersize,  bannersuffix, route)
		})
	end

	return shieldSpec
end

local missingShields

local shieldExistsCache = {}

local function render(shieldEntry, scale, showLink)
	local shield = shieldEntry.shield
	local banners = shieldEntry.banners

	local size
	if shield[2] then
		local width, height = mw.ustring.match(shield[2], "(%d*)x?(%d*)")
		width = tonumber(width)
		height = tonumber(height)
		local sizeparts = {}
		if width then
			insert(sizeparts, format("%d", width * scale))
		end
		if height then
			insert(sizeparts, format("x%d", height * scale))
		end
		size = concat(sizeparts)
	else
		size = format("%s%d", landscape and "x" or "", defaultShieldSize * scale)
	end
	local shieldCode = format("[[File:%s|%spx|link=|alt=]]", shield[1], size)
	if not banners[1] then return shieldCode end

	for _,banner in ipairs(banners) do
		shieldCode = format("[[File:%s|%spx|link=|alt=]]<br>%s",
			banner[1],
			defaultShieldSize,
			shieldCode)
	end
	return '<span style="display: inline-block; vertical-align: baseline; line-height: 0; text-align: center;">' .. shieldCode .. '</span>'
end

function p.shield(args, scale, showLink, mainShield, shieldList)
	missingShields = {}

	scale = scale or 1

	local rendered = {}
	for _,entry in ipairs(shieldSpec(args, mainShield, shieldList)) do
		insert(rendered, render(entry, scale, showLink))
	end
	return concat(rendered), missingShields
end

-- Links/abbreviations
function p.link(args)
	local nolink = args.nolink
	local abbr = parser(args, 'abbr')
	if nolink then
		return abbr
	else
		local link = parser(args, 'link')
		if not link or link == '' then
			return abbr
		else
			return format("[[%s|%s]]", link, abbr)
		end
	end
end

local function stateName(args)
	-- TODO transition
	local data = mw.loadData(statenameModuleName) 
	local abbr = args.state or args.province
	local countryData = data[args.country]
	return countryData and countryData[abbr]
end

--------------------------
--[[-
@type status
@field #string row: The start of the row, for this particular type (color)
@field #string established: The string to be output in the "Formed" column.
	For future routes, "proposed" is displayed here.
	Otherwise, display the year passed in the established parameter.
@field #string removed: The string to be output in the "Removed" column.
	In the case of routeStates.former, the year that the route was
	decommissioned is output instead.
]]
--[[-
Route statuses.
@list <#status>
]]
local routeStatuses = {
	-- current routes
	current = {
		row = "|-",
		removed = "current"
	},
	-- future routes
	future = {
		row = '|- style="background-color:#ffdead;" title="Future route"',
		established = "proposed",
		removed = "—"
	},
	-- former routes
	former = {
		row = '|- style="background-color:#d3d3d3;" title="Former route"'
	},
	-- routes marked as former by override
	-- deprecated
	formeroverride = {
		row = '|- style="background-color:#d3d3d3;" title="Former route"',
		removed = "—"
	},
	-- route with unknown status
	unknown = {
		row = "|-",
		removed = "—"
	} 
}

--[[-
Return the route status.
@param #string established `established` argument passed to the module
@param #string decommissioned `decommissioned` argument passed to the module
@return #status the status of the route.
]]
local function getRouteStatus(established, decommissioned)
	if decommissioned == 'yes' then
		-- a former route with no decommission information
		return routeStatuses.formeroverride
	elseif decommissioned then
		-- If the route is decommissioned, then it must be a former route.
		return routeStatuses.former
	elseif not established then
		-- Without the establishment date, there is not enough information
		-- to determine the status of the route.
		return routeStatuses.unknown
	elseif established == 'proposed' then
		-- a future route
		return routeStatuses.future
	else
		-- a current route
		return routeStatuses.current
	end
end

--[[-
A limited replacement for {{dts}}.
Derive the sort key from a given date.
@param #string date
@param #string circa "yes" if `date` is tagged as circa
@return #string true the hidden sort key, along with the year of the original date
@return #boolean false if the sort key cannot be derived
]]
local function dtsYearCore(date)
	local year = lang:formatDate('Y', date) -- year for this date
	if year == date then -- If the provided date is just the year,
		-- tack on January 1 for the sort key to work right.
		date = date .. "-01-01"
	end
	local month = lang:formatDate('m', date) -- month for this date
	local day = lang:formatDate('d', date) -- day for this date
	-- Create and store the formatted hidden sort key.
	-- The year must be five digits, per convention.
	local dtsStr = format("%05d-%02d-%02d", year, month, day)
	-- Return the hidden sort key and the year for this date.
	return {dtsStr, year}
end

local function dtsYear(date, circa)
	local success, result = pcall(dtsYearCore, date)
	if not success then
		result = {
			"00001-01-01",
			util.err(format('Invalid date "%s".', date))
		}
	end
	-- Generate the HTML code necessary for the hidden sort key.
	local dtsStyle = format("style=\"white-space:nowrap;\" data-sort-value=\"%s\"", result[1])
	local year = result[2]
	if circa == 'yes' then -- If the date is tagged as circa,
		-- add the circa abbreviation to the display. Derived from {{circa}}.
		year = "<span style=\"white-space:nowrap;\"><abbr title=\"circa\">c.</abbr>&thinsp;" .. year .. "</span>"
	end
	return dtsStyle, year
end

--- Return formatting and output for a date column.
local function date(text, date, circa, ref)
	-- Returns the text if specified, or the dtsYear-formatted date, and an em-dash.
	local style, output
	if text then
		output = text
	elseif date then
		style, output = dtsYear(date, circa)
	else
		output = "—"
	end
	return format("|align=center %s|%s%s", style or "", output, ref)
end

--- Return output for the date columns for a given route.
local function dates(established, decommissioned, routeStatus, args)
	local established_ref = args.established_ref or '' -- Reference for date established
	local decommissioned_ref = args.decommissioned_ref or '' -- Reference for date decommissioned
	return format("%s\n%s",
		date(routeStatus.established, established, args.circa_established, established_ref),
		date(routeStatus.removed, decommissioned, args.circa_decommissioned, decommissioned_ref))
end

--- Return output for the termini columns for a given route.
local function termini(args)
	local beltway = args["beltway"]
	if beltway then
		-- The given route is a beltway. 
		-- `beltway` text will span both termini columns.
		return "|colspan=2 align=center|" .. beltway
	else
		local terminus_a = args["terminus_a"] or '—' -- Southern or western terminus
		local terminus_b = args["terminus_b"] or '—' -- Northern or eastern terminus
		-- Fill in the termini columns
		return '|' .. terminus_a .. '||' .. terminus_b
	end
end

--- Return output for the length columns for a given route, with the appropriate conversions.
local function length(args)
	local km = args["length_km"] or '' -- Length in kilometers
    local mi = args["length_mi"] or '' -- Length in miles
    local ref = args["length_ref" ] or ''

    if mi == '' and km == '' then
        return format("|align=right|—||align=right|—")
	elseif mi ~= '0' and km == '' then
		return format("|") .. frame:expandTemplate{ title = 'convert', args = { mi, "mi", "km", disp = "table"}}
	else
		return format("|") .. frame:expandTemplate{ title = 'convert', args = { km, "km", "mi", disp = "table"}}
	end
end


--- Generate a "Local names" cell if necessary.
local function localname(args)
	local enabled = args[1] or ''
	if enabled == "local" then
		local localName = args["local"] or ''
		return "|" .. localName
	else
		return ''
	end
end

--- Generate a "Notes" cell if necessary.
local function notes(notes)
	if notes == 'none' then
		return '| ' --create empty cell
	elseif notes then
		return '|' .. notes --display notes in cell
	else
		return '' --create no cell
	end
end

--- Derive the sort key from a given route.
local function sortkey(abbr)
	-- Split `abbr` into three possibly empty parts, with number in the middle.
	local prefix, num, suffix = mw.ustring.match(abbr, "([^0-9]*)(%d*)(.*)")
	-- If `abbr` does not contain a number, the entry appears at the bottom.
	num = tonumber(num)
	num = type(num) == "number" and format("%04d", num) or ""
	-- The sort key is `abbr`, but with route number zero-padded to 4 digits
	-- and prefix moved to the end.
	return mw.text.trim(
		mw.ustring.gsub(format("%s%s %s", num, suffix, prefix), "&nbsp;", " "),
		"- ")
end

local function route(args, shieldSize)
	local link, abbr = p.link(args)
	-- Use the sort key if already specified.
	local sortkey = args.sortkey or sortkey(abbr or "")
	local shield = p.shield(args)

	if shield == nil or args.noshield then
		return format('!scope="row" class="nowrap" data-sort-value="%s"|%s',
			sortkey, link)
	else
		return format('!scope="row" class="nowrap" data-sort-value="%s"|%s %s',
			sortkey, shield, link)
	end
end

--- Derive the anchor from a given route.
local function anchor(routeType, routeNo)
	-- Split `routeNo` into three possibly empty parts, with number in the middle.
	local prefix, num, suffix = mw.ustring.match(routeNo, "([^0-9]*)(%d*)(.*)")
	-- Zero-pad route number to 4 digits if `routeNo` does contain a number.
	num = tonumber(num)
	num = type(num) == "number" and format("%04d", num) or ""
	-- The anchor is the concatenation of `type` and zero-padded `routeNo`.
	return format("%s%s%s%s", routeType, prefix, num, suffix)
end

function p._row(args)
	local established = args.established
	local decommissioned = args.decommissioned
	local routeStatus = getRouteStatus(established, decommissioned)
	local anchor = args.anchor or anchor(args.type, args.route)
	local rowdef = format('%s id="%s"', routeStatus.row, anchor)
	local route = route(args)
	local length = length(args)
	local termini = termini(args)
	local localname = localname(args)
	local dates = dates(established, decommissioned, routeStatus, args)
	local notes = notes(args.notes)

	local row = {rowdef, route, length, termini, localname, dates, notes}
	return concat(row, '\n')
end

function p.row(frame)
	-- Import module function to work with passed arguments
	local getArgs = require('Module:Arguments').getArgs
	local args = getArgs(frame) -- Gather passed arguments into easy-to-use table
	return p._row(args);
end

return p