Module:Sandbox/AbstractWikipedia/TemplateEvaluator
This is the template evaluator module of the Abstract Wikipedia template-renderer prototype.
Given a string in template language it passes it on to the template parser, then evaluates all elements (text and slots) of the template and finally applies all specified dependency relations.
It returns a lexeme list, which is basically a list of elements, each element being itself either a lexeme (defined in the Lexemes module) or a lexeme list, augmented with a field root
indicating the lexeme (or lexeme list) which is the root of the template.
In Wikifunctions, this module would probably have to be part of the Orchestrator code, since it must be able to execute the user-defined functions of Wikifunctions.
local p = {}
local parser = require("Module:Sandbox/AbstractWikipedia/TemplateParser")
local renderer = require("Module:Sandbox/AbstractWikipedia/Renderers")
local evaluateFunctionArgument, evaluateFunction -- forward decleration
-- Helper function to easily concatenate nil values in error messages
local function safe ( value )
if value == nil then
return "nil"
else
return value
end
end
-- Evaluates an interpolation: returns its value and rewrites the interpolation
-- element to an appropriate type.
-- Index is passed for debugging purposes
local function evaluateInterpolation ( element, args, index )
if not args[element.arg] then
error("Reference to undefined argument "..element.arg.." in element "..index)
end
if type(args[element.arg]) == "table" then
-- The renderers may contain special table arguments, either
-- constructors or variant template lists
local arg = args[element.arg]
if arg["_predicate"] ~= nil then
element.type = 'constructor'
element.constructor = arg
return arg
elseif #arg > 0 and arg[1].template ~= nil then
-- A variant template list
element.text = renderer.selectTemplate(arg, args, element.role)
else
error("Argument "..element.arg.." is of unknown table type: ".. safe(mw.logObject(arg)) )
end
else
element.text = tostring(args[element.arg])
end
if element.text:match("{.+}") then
element.type = 'subtemplate'
elseif element.text:match("^L%d+$") then
element.type = 'lexeme'
elseif element.text:match("^Q%d+$") then
element.type = 'item'
elseif element.text:match("^[+-]?%d+") then
element.type = 'number'
else -- Note that we do not treat punctuation or spaces specially here
element.type = 'text'
end
return element.text
end
-- Evaluates an interpolation
-- Index is passed for debugging purposes
evaluateFunction = function ( element, args, index )
if not functions[element["function"]] then
error("Reference to undefined function "..element["function"].." in element "..index)
end
local evaluated_args = {}
for _, arg in ipairs(element.args) do
table.insert(evaluated_args, evaluateFunctionArgument(arg, args, index))
end
return functions[element["function"]](unpack(evaluated_args))
end
-- Evaluates a function argument
-- args are arguments passed through the frame
-- Index is passed for debugging purposes
evaluateFunctionArgument = function ( arg, args, index )
if (arg.type == 'text' or arg.type == 'number' or arg.type == 'lexeme' or arg.type == 'item') then
return arg.text
elseif (arg.type == 'interpolation') then
-- We allow functions to used undefined arguments, but log it.
if not args[arg.arg] then
mw.log("Usage of undefined argument "..arg.arg.." in function")
return 'nil'
end
return evaluateInterpolation (arg, args, index)
elseif (arg.type == 'function') then
return evaluateFunction (arg, args, index)
else
error("Undefined element "..arg.text.." at index "..arg.text)
end
end
-- Helper function that takes a list of lexemes, and returns it as a list or
-- as a singleton lexeme, if the list is of length 1.
-- If the list is empty, returns nil.
local function collapseIfSingle ( lexemes )
if not lexemes or #lexemes == 0 then
return nil
elseif #lexemes == 1 then
return lexemes[1]
else
return lexemes
end
end
-- Evaluates elements passed by the parser to return lexemes
local function evaluateElements(elements, args)
local result = {}
for _, element in ipairs(elements) do
if (element.type == 'interpolation') then
-- Rewrite interpolation as something else
evaluateInterpolation(element, args, element.index)
end
if (element.type == 'punctuation' or element.type == 'spacing' or element.type == 'text') then
table.insert(result, functions.TemplateText(element.text, element.type))
elseif (element.type == 'number') then
table.insert(result, functions.Cardinal(element.text))
elseif (element.type == 'lexeme') then
table.insert(result, functions.Lexeme(element.text))
elseif (element.type == 'item') then
table.insert(result, functions.Label(element.text))
elseif (element.type == 'constructor') then -- originally an interpolation
local lexemes = p.evaluateConstructor(element.constructor, args, element.role)
table.insert(result, collapseIfSingle(lexemes))
elseif (element.type == 'subtemplate') then -- originally an interpolation
local lexemes = p.evaluateTemplate(element.text, args)
table.insert(result, collapseIfSingle(lexemes))
elseif (element.type == 'function') then
local function_result = evaluateFunction(element, args, element.index)
if function_result == nil then
error("Function "..element["function"].." returned no value")
end
table.insert(result, function_result)
else
error("Undefined element "..element.text.." at index "..element.text)
end
end
return result
end
-- Fetches recursively the root lexeme of a lexeme_list
local function fetchRoot ( lexeme_list )
if lexeme_list.root then
return fetchRoot(lexeme_list[lexeme_list.root])
else
-- It is not a list, but a single lexeme
return lexeme_list
end
end
local function applyRelations ( lexemes, relations_to_apply )
for _, relation in ipairs(relations_to_apply) do
if not relations[relation["role"]] then
-- We skip undefined labels, but we log it for visibility
mw.log("Reference to unknown relation "..relation["role"].." in element "..relation.target)
else
relations[relation.role](fetchRoot(lexemes[relation.source]), fetchRoot(lexemes[relation.target]))
end
end
end
-- This function encapsulates the steps needed to evaluate a template prior to
-- lexeme-selection stage.
function p.evaluateTemplate ( template, args )
local elements, relations_to_apply, root_index = parser.parse(template)
-- Note that the "lexemes" can themselves be lexeme lists, if a sub-template
-- is used
lexemes = evaluateElements(elements, args)
applyRelations( lexemes, relations_to_apply )
lexemes["root"] = root_index
return lexemes
end
-- helper function to merge two tables (constructing a copy).
-- Fields in t1 have priority
local function joinTables(t1, t2)
local result = {}
for k, v in pairs(t1) do
result[k] = v
end
for k, v in pairs(t2) do
if result[k] then
mw.log("joinTables: key "..k.." present in both tables")
else
result[k] = v
end
end
return result
end
-- This function encapsulates the steps needed to evaluate a constructor prior
-- to lexeme-selection stage.
function p.evaluateConstructor ( constructor, args, role )
mw.log("Constructor:")
mw.logObject(constructor)
local predicate = constructor["_predicate"] or error("Constructor doesn't have a _predicate field")
-- "renderers" should be in global scope
local templates = renderers[predicate]
if not templates then
-- We gracefully return an empty list (of lexemes), and log the error.
-- This allows expanding abstract content without necessarily updating
-- all languages.
mw.log("No templates found for predicate "..predicate)
return {}
end
local main = renderer.selectMainTemplate(templates) or error("No main template found for predicate "..predicate)
-- The arguments for evaluation are the fields of the constructor plus any
-- other arguments (typically templates) provided in the template list.
local constructor_args = joinTables(constructor, templates)
-- As fallback arguments we may chain those which come from the calling scope
-- but I'm not sure it's a good idea. Commented out for now.
-- setmetatable(constructor_args, { __index = args })
return p.evaluateTemplate(renderer.selectTemplate(main, constructor_args, role), constructor_args)
end
return p