# Module:Road data/parser

local p = {} -- Package to be exported  

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

local parserHooksModuleName = "Module:Road data/parser/hooks" .. moduleSuffix  

-- Local library aliases  
local format = string.format  
local gsub = mw.ustring.gsub  
local upper = mw.ustring.upper  

---
-- Substitution pattern based on passed arguments  
-- Syntax: [param|value|match|mismatch]  
-- where  
-- param is the parameter name to be tested  
-- value is the value to test against argument; if empty, the argument is  
-- tested for existence  
-- match is the string to be substituted if the argument matches value  
-- mismatch is the string to be substituted if the argument does not match  
-- the value  
-- These arguments may not contain "[", "|", or "]".  
local prepattern = "%[(%w+)%|([^%[|%]]*)%|([^%[|%]]*)%|([^%[|%]]*)%]"  
---
-- Parameter substitution pattern  
-- Syntax: %param%  
-- where param is the name of the parameter whose value is to be substituted  
-- in place of %param%.  
local pattern = "%%(%w+)%%"  

---
-- Perform substitutions.  
-- @param #string formatStr The string the be substituted  
-- @param #table args The arguments passed to this module  
local function subst(formatStr, args)  
---
-- Perform a substitution based on passed argument.  
-- @param #string param The parameter name to be tested  
-- @param #string value The value to test against argument; if empty,  
-- the argument is tested for existence  
-- @param #string ifmatch The resulting string if the argument matches  
-- `value`  
-- @param #string ifmismatch The resulting string if the argument does not  
-- match `value`  
-- @return #string either `ifmatch` or `ifmismatch`, based on the test  
local function testArgs(param, value, ifmatch, ifmismatch)  
local arg = args[param] or ''  
if value ~= '' then  
return arg == value and ifmatch or ifmismatch  
else  
return arg ~= '' and ifmatch or ifmismatch  
end  
end  
-- argument-test substitutions  
local preprocessed = gsub(formatStr, prepattern, testArgs)  
-- parameter substitutions  
return (gsub(preprocessed, pattern, args))  
-- gsub returns number of matches as second value.  
-- The enclosing parens discards it.  
end  

---
-- Determine whether a given title exists on Wikipedia.  
-- @param #string name The title, e.g., article name and file name,  
-- without namespace prefix  
-- @param #string key The name of the entry being translated.  
-- @return #boolean `true` if the title exists, false otherwise  
local function titleExists(name, key)  
if name == '' then return false end  
local namespaceModule = mw.loadData('Module:Road data/parser/namespace')  
-- Retrieve the namespace for `key`.  
local namespace = namespaceModule[key] or 0  
local title = mw.title.new(name, namespace);  
return title.exists  
end  

---
-- Determine whether titles exist on Wikipedia.  
-- @param value A string or a table containing strings of titles to be checked  
-- against  
-- @param #string key The name of the entry being translated.  
-- @return #boolean `true` if all titles exist, false otherwise  
local function ifexists(value, key)  
local valueType = type(value)  
if valueType == "table" then  
-- If `value` is a table, recursively check the existence  
-- for each element within the table.  
for _,entry in pairs(value) do  
if not ifexists(entry, key) then return false end  
end  
return true  
end  
-- Otherwise, `value` is a string, so check the existence for that string.  
return titleExists(value, key)  
end  

---
-- Perform a translation on a given entry.  
-- @param entry An entry to be translated; may be any non-function type.  
-- A table may be a parser hook specification, a switch table, or an  
-- ordinary value table. Translations are applied recursively.  
-- @param #table args The arguments passed to this module  
-- @param #string key The name of the entry being translated.  
-- @return The translated entry  
local function translate(entry, args, key)  
if type(entry) == "string" then  
return subst(entry, args) -- Substitute arguments as necessary.  
elseif type(entry) ~= "table" then  
return entry  
elseif entry.hook then  
-- This entry is a parser hook.  
-- Requires: Parser hook must have hook field.  
local hook = entry.hook  
local parserHooksModule = require(parserHooksModuleName)  
local hookFunction = parserHooksModule[hook]  
or error("Hook '" .. hook .. "' does not exist", 0)  
return translate(hookFunction(entry, args), args, key)  
elseif entry.arg or entry.undefined or entry.default then  
-- This entry is a switch table.  
-- Requires: Switch table must have  
-- arg, undefined, or default fields  
-- but not hook field.  
local arg = args[entry.arg or "route"]  
if entry[arg] then return translate(entry[arg], args, key) end  
if arg == nil and entry.undefined ~= nil then  
-- result for unspecified argument  
return translate(entry.undefined, args, key)  
end  
-- default result for mismatch  
local defaultValue = translate(entry.default, args, key)  
if defaultValue and entry.ifexists and not args.ignoreifexists then  
-- Check existence.  
if ifexists(defaultValue, key) then return defaultValue end  
-- Failed existence check results in fallback value (default to nil).  
return entry.otherwise and translate(entry.otherwise, args, key) or nil  
else  
return defaultValue  
end  
else  
-- This entry is a value table.  
-- Process each table element.  
local result = {}  
for key,elem in pairs(entry) do  
result[key] = translate(elem, args, key)  
end  
return result  
end  
end  

---
-- Retrieve an entry from a data module based on a given type and key.  
-- @param #string module The name of the data module to be fetched  
-- @param type The key for the type table within the loaded table  
-- @param key The key for the entry within the type table  
-- @return fetchedTable[type][key] if specified, where `fetchedTable` is the  
-- table fetched from `module`, nil otherwise  
local function getTypeData(module, type, key)  
-- Attempt to fetch the given data module.  
local success, moduleData = pcall(mw.loadData, module)  
if not success then return false, moduleData end -- Module could not be loaded  
-- The type table defaults to empty-key table if undefined.  
local typeTable = moduleData[type] or moduleData['']  
-- Fallback table is the empty-key table, with the empty table as default.  
local defaultTable = moduleData[''] or {}  
if typeTable then  
local alias = typeTable.alias  
if alias and alias.module and alias.type then  
-- The type table is an alias table.  
-- Recursively fetch the aliased type data.  
local aliasedModule = "Module:Road data/strings/" .. alias.module  
local aliasedType = alias.type  
return getTypeData(aliasedModule, aliasedType, key)  
end  
return true, typeTable[key] or defaultTable[key] or nil  
else  
return true, nil  
end  
end  

---
-- Determine the module name for the lookup by country and state.  
-- @param #table args The arguments passed to this module  
-- @return #string The module name to be fetched  
local function getModuleName(args)  
-- countries with submodules for states or provinces  
local stateCountries = {USA = true, CAN = true}  
local state = upper(args.state or args.province or '')  
local country  
if args.country then  
country = upper(args.country)  
else  
-- Recover the country from the given state or province.  
local countryModule = mw.loadData("Module:Road data/countrymask")  
country = countryModule[state] or 'UNK'  
end  
if stateCountries[country] and state ~= '' then  
-- Submodule within the country exists.  
return format("Module:Road data/strings/%s/%s", country, state)  
end  
return format("Module:Road data/strings/%s", country)  
end  

---
-- Fetch the entry from the appropriate module, and return that entry  
-- substituted with appropriate values.  
-- @param #table args The arguments to be used for lookup and substitutions  
-- @param #string key The key for the entry within the type table  
-- @param #string type (optional) The key for the type table within the fetched  
-- module; defaults to args.type  
-- @param #string moduleName (optional) The name of the module to be fetched;  
-- defaults to the module determined by country and state  
-- @return The substituted entry  
function p.parser(args, key, type, moduleName)  
-- Determine module name, if not given.  
local dataModuleName = moduleName or getModuleName(args)  
-- Fetch the entry from the module.  
local success, formatStr = getTypeData(dataModuleName, type or args.type, key)  
if not success then return false, formatStr end  
-- Translate the entry.  
return translate(formatStr, args, key)  
end  

return p