Jump to content

Module:Sandbox/AbstractWikipedia/TemplateEvaluator

From Meta, a Wikimedia project coordination wiki
Module documentation

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