Module:Road data/parser
local p = {} -- Package to be exported
 
 -- Change to &quot;&quot; upon deployment.
 local moduleSuffix = &quot;&quot;
 
 local parserHooksModuleName = &quot;Module:Road data/parser/hooks&quot; .. 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 &quot;[&quot;, &quot;|&quot;, or &quot;]&quot;.
 local prepattern = &quot;%[(%w+)%|([^%[|%]]*)%|([^%[|%]]*)%|([^%[|%]]*)%]&quot;
 ---
 -- 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 = &quot;%%(%w+)%%&quot;
 
 ---
 -- 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 == &quot;table&quot; 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) == &quot;string&quot; then
 		return subst(entry, args) -- Substitute arguments as necessary.
 	elseif type(entry) ~= &quot;table&quot; 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(&quot;Hook '&quot; .. hook .. &quot;' does not exist&quot;, 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 &quot;route&quot;]
 		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 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 = &quot;Module:Road data/strings/&quot; .. 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(&quot;Module:Road data/countrymask&quot;)
 		country = countryModule[state] or 'UNK'
 	end
 	if stateCountries[country] and state ~= '' then
 		-- Submodule within the country exists.
 		return format(&quot;Module:Road data/strings/%s/%s&quot;, country, state)
 	end
 	return format(&quot;Module:Road data/strings/%s&quot;, 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