Module:User:Surjection/luasubst


This is a private module sandbox of Surjection, for their own experimentation. Items in this module may be added and removed at Surjection's discretion; do not rely on this module's stability.


local export = {}

local function isPositiveInteger(v)
	return type(v) == 'number' and v >= 1 and math.floor(v) == v and v < math.huge
end

local function shift_args(args, n)
	local newargs = {}
	for k, v in pairs(args) do
		if isPositiveInteger(k) then
			if k > n then
				newargs[k - n] = v
			end
		else
			newargs[k] = v	
		end
	end
	return newargs
end

local PseudoframeArgument = {}

function PseudoframeArgument.expand(obj)
	return obj["_expand"](obj)
end

function PseudoframeArgument.new(frame, obj)
	return setmetatable({ ["_value"] = obj, ["_expand"] = function(obj) return frame:preprocess(obj) end }, PseudoframeArgument)
end

PseudoframeArgument.__index = PseudoframeArgument

local Pseudoframe = {}
local forwarded_methods = {
    "callParserFunction",
    "expandTemplate",
    "extensionTag",
    "getTitle",
    "newChild",
    "preprocess",
    "newParserValue",
    "newTemplateParserValue",
}
for _, name in ipairs(forwarded_methods) do
	Pseudoframe[name] = function (pf, ...)
		local f = pf["_frame"]
		return f[name](f, ...)
	end
end

function Pseudoframe.getArgument(pf, arg)
	if type(arg) == "table" then
		return pf:getArgument(arg.name)
	end
	local arg = pf.args[arg]
	if arg == nil then return nil end
	return PseudoframeArgument.new(f, arg)
end

function Pseudoframe.argumentPairs(pf)
	return pairs(pf.args)
end

function Pseudoframe.getParent(pf)
	if pf._parent then
		return pf._parent	
	else
		return pf._frame:getParent()
	end
end

function Pseudoframe.new(frame, args, parent)
	return setmetatable({ ["_frame"] = frame, ["args"] = args, ["_parent"] = parent }, Pseudoframe)
end

Pseudoframe.__index = Pseudoframe

local function trampoline(module_name, entrypoint, pframe)
	return mw.text.nowiki(require(module_name)[entrypoint](pframe))
end

local function clean_traceback(msg)
	local tb = debug.traceback(msg, 3)
	tb = mw.ustring.gsub(tb, "Module:User:Surjection/luasubst:.+", "")
	tb = mw.ustring.gsub(tb, "package.lua:.+", "")
	tb = mw.ustring.gsub(tb, "mw.lua:.+", "")
	return mw.text.trim(tb)
end

local function require_tracked(trackstring, old_require)
	return function (name)
		trackstring[1] = trackstring[1] .. clean_traceback("Module imported " .. name) .. "\n\n"
		return old_require(name)
	end
end

local function trampoline_track_requires(module_name, entrypoint, pframe)
	local old_require = require
	local slot = {""}
	require = require_tracked(slot, old_require)
	local result = old_require(module_name)[entrypoint](pframe)
	require = old_require
	return mw.text.tag("pre", {}, slot[1])
end

local function do_subst_explicit(frame, trampoline)
	local module_name = "Module:" .. frame.args[1]
	local entrypoint = frame.args[2]
	local pframe = Pseudoframe.new(frame, shift_args(frame.args, 2), nil)
	local result = trampoline(module_name, entrypoint, pframe)
	return result
end

local function do_subst_implicit(frame, trampoline)
	local parent = frame:getParent()
	local template_name = parent.args[1]
	local template_args = shift_args(parent.args, 1)
	local pseudoparent = Pseudoframe.new(parent, template_args, nil)
	
	local template_title = mw.title.new(template_name, 10)
	if template_title.isRedirect then
		template_title = template_title.redirectTarget
	end
	local template_source = template_title:getContent()
	template_source = mw.ustring.gsub(template_source, "<noinclude>.-</noinclude>", "")
	template_source = mw.ustring.gsub(template_source, "<includeonly>(.-)</includeonly>", "%1")
	template_source = mw.ustring.gsub(template_source, ".-<onlyinclude>(.-)</onlyinclude>.*", "%1")
	local ok, _, invoke_text = mw.ustring.find(template_source, "^(%b{})")
	if not ok then error("Template does not begin with an invoke or a transclusion!") end
	local ok, _, invoke_params = mw.ustring.find(invoke_text, "^{{#invoke:(.+)}}")
	if not ok then error("Template does not begin with an invoke!") end
	
	local ok, _, module_name, entrypoint, module_params = mw.ustring.find(invoke_params, "^([^|]+)|([^|]+)|(.+)")
	if not ok then
		ok, _, module_name, entrypoint = mw.ustring.find(invoke_params, "^([^|]+)|([^|]+)$")
		if ok then
			module_params = nil
		else
			error("Could not parse invoke!")	
		end
	end
	
	local parsed_args = {}
	if module_params then
		local parseTemplate = require("Module:User:Surjection/templateparser").parseTemplate
		local parsed_ok
		parsed_ok, parsed_args = parseTemplate("{{args|" .. module_params .. "}}")
		if not parsed_ok then error("Could not parse module invoke parameters!") end
		for k, v in pairs(parsed_args) do
			if v:find("%[") or v:find("{") then
				parsed_args[k] = frame:preprocess(v)
			end
		end
	end
	
	module_name = "Module:" .. module_name
	local pframe = Pseudoframe.new(frame, parsed_args, pseudoparent)
	local result = trampoline(module_name, entrypoint, pframe)
	return result
end

function export.subst(frame)
	return do_subst_explicit(frame, trampoline)
end

function export.subst_universal(frame)
	return do_subst_implicit(frame, trampoline)
end

function export.subst_universal_track_requires(frame)
	return do_subst_implicit(frame, trampoline_track_requires)
end

return export