Jump to content

Module:Sandbox/Grufo

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Uzume (talk | contribs) at 11:59, 16 July 2025 (updates: no need to pass base frame; call "require" inside monkey patch and patch "package.loaded[modtitle]"; concatenate multiple return values as strings). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

-- Example module to demonstrate how it can detect how it is loaded and why such can be useful
require[[strict]]
local currentFrame = mw.getCurrentFrame()
local pkgname = ... or currentFrame:getTitle()
local pkgapi
if not ... then
	--[=[ '#invoke' and debug console
		This is where "wrapper" functionality should be so it is available to wikitext templates, etc.
	]=]
	pkgapi = require(pkgname) -- '#invoke' does not add the package to "package.loaded"
	if not currentFrame:getParent() then
		return pkgapi --XXX: debug console caveat, due to "require" this is the saved version of the module
	end
	local mt = {}
	function mt:__index(key)
		return function(frame) --[=[
			This is the function from '{{#invoke:key|...}}', where 'key' is any valid string argument
			'#invoke' parses this first "function" argument differently (e.g., "=" is valid, etc.)
			and sending the rest to the frame parser
			Notice: '{{#invoke:main|...}}' is valid despite
			"main" being an API function accessible from require()
			]=]
			assert(frame == currentFrame)
			return pkgapi.main(currentFrame.args, key)
		end
	end
	return setmetatable({}, mt)
else
	--[=[ 'require()' (and 'mw.loadData()'?)
		This is where "main" functionality should be so it is available to modules, including itself
	]=]
	pkgapi = {}
	function pkgapi.main(args, key)
		local numparamsn, numparams = 0, {}
		local strparamsn, strparams = 0, {}
		for param in pairs(args) do
			local paramtype = type(param)
			if 'number' == paramtype then
				numparamsn = 1 + numparamsn
				numparams[numparamsn] = param
			elseif 'string' == paramtype then
				strparamsn = 1 + strparamsn
				strparams[strparamsn] = param
			end
		end
		table.sort(numparams)
		table.sort(strparams)
		local ret = pkgname .. ' {{key|' .. key .. '}} {{nums'
		for i, numparam in ipairs(numparams) do
			ret = ret .. '|' .. numparam .. '=' .. args[numparam]
		end
		ret = ret .. '}} {{strs'
		for i, strparam in ipairs(strparams) do
			ret = ret .. '|' .. strparam .. '=' .. args[strparam]
		end
		ret = ret .. '}}'
		return ret
	end
	function pkgapi.invoke(emu, modname, fnname, modargs)
		local baseFrame = currentFrame:getParent() or currentFrame -- No need to pass a frame with no parent
		if not emu then
			-- Copy "modargs" so we don't suddenly change them for the caller
			local parserfnargs = {}
			for k, v in pairs(modargs) do
				parserfnargs[k] = v
			end
			table.insert(parserfnargs, 1, fnname)
			table.insert(parserfnargs, 1, modname)
			return baseFrame:callParserFunction('#invoke', parserfnargs)
		end
		--[=[
		Emulate a "#invoke" call with "require", a frameSpoof and a monkey patched "getCurrentFrame"
		providing a spoofed "args", "getParent" and "getTitle"
		falling back to creating an on demand "newChild" frame for other fields
		]=]
		--[=[ TODO: Input validation:
		Scribunto does a good job of its own input validation and throws errors when we pass these on
		but we spoof "modargs" to the target *raw*, so do some validation so the target gets as few
		surprises as possible.
		]=]
		--XXX: mw.title.new(828, modname).prefixedText
		--XXX: mw.site.namespaces.Module.canonicalName .. ':' .. modname
		local modtitle = 'Module:' .. modname

		--[=[ Spoof a "#invoke" frame for the target:
		Scribunto frame objects are just regular Lua tables with method functions, however, those
		functions are also closures that depends on their upvalues and require one to pass in the
		correct frame reference, so we cannot copy those function references into our "spoofFrame"
 		but we can build redirector stub functions that punt calls to those, so long as we ensure
 		we always call them with the right frame reference. We want the target module to never use
 		these stubs as creating a frame via "newFrame" is expensive, so we go one further and
 		instead create delay stub functions that only do that on demand when the target module
 		tries to access any fields we do not end up directly spoofing. The delay stubs also have to
 		punt to newly created frame but only after it has created it. Creating the new frame should
 		only happen once so even though we create many delay stubs, only one will actually create
 		it. To ensure that only one winning delay stub is ever called, each delay stub updates all
 		fields of the "spoofFrame" to always punting stubs instead, thereby effectively caching
 		the frame creation. So we start by filling all frame fields of "spoofFrame" with delay stubs
 		that: first create a new frame, then cache it by update all frame fields with always punting
 		stubs, and finally punt themselves. Of course the "args" fields is not a function so it we
 		must handle it different but we at first ignore that and later ensure to overwrite it in
 		both cases: when writing always punting stubs we just write the actual frame "args" value and
 		when writing delay stubs we just write in our local "args" spoofer. Of course, we overwrite
 		the delay stubs with a few more local spoofs that are easy to create and commonly accessed
 		by the target: "getParent" and "getTitle". We might be able to improve performance by
 		spoofing more fields but many call back into the PHP parser so they would be hard to emulate
 		adequately without creating an actual parser frame and the ones we do spoof cover those
 		most frequently accessed by any target module: the "args" and the parent "args".
		]=]
		local frameSpoof = {}
		-- Punt to "newChild" frame on demand for all function fields; Spoofed fields are overwritten later
		for name in pairs(baseFrame) do
			frameSpoof[name] = function(self, ...)
				local frameDelayed = baseFrame:newChild{title = modtitle, args = modargs}
				-- Overwrite all fields on demand effectively caching this so it will only happen once
				for name in pairs(frameDelayed) do
					frameSpoof[name] = function(self, ...)
						if self == frameSpoof then
							self = frameDelayed -- Only redirect correct value and let error propagate
						end
						-- All future accesses will punt since we made an actual frame we lose nothing
						return frameDelayed[name](self, ...)
					end
				end
				-- Overwrite "args" again because it is not a function
				frameSpoof.args = frameDelayed.args
				if self == frameSpoof then
					self = frameDelayed -- Only redirect correct value and let error propagate
				end
				-- First punt after updating all fields to punt in the future
				return frameDelayed[name](self, ...)
			end
		end
		-- Spoof "getParent"
		function frameSpoof:getParent(...)
			if self ~= frameSpoof then
				return baseFrame.getParent(self, ...) -- Let a real frame throw the error
			end
			return baseFrame
		end
		-- Spoof "getTitle"
		function frameSpoof:getTitle(...)
			if self ~= frameSpoof then
				return baseFrame.getTitle(self, ...) -- Let a real frame throw the error
			end
			return modtitle
		end
		-- Spoof "args" including recreating a metatable
		local args_mt = {}
		args_mt.__index = modargs
		function args_mt:__pairs()
			return pairs(modargs)
		end
		function args_mt:__ipairs()
			return ipairs(modargs)
		end
		frameSpoof.args = setmetatable({}, args_mt)

		-- Emulate the "#invoke" call with "require" and our "frameSpoof"
		local mpkey = 'getCurrentFrame'
		local getCurrentFrame = rawget(mw, mpkey)
		-- Monkey patch "getCurrentFrame" to return our "frameSpoof"
		rawset(mw, mpkey, function() return frameSpoof end)
		local oldmod = rawget(package.loaded, modtitle)
		-- "require" here so the target initialization block sees our monkey patched "getCurrentFrame"
		local fn = require(modtitle)[fnname]
		rawset(package.loaded, modtitle, oldmod) -- Pretend "require" was never called here
		local ret
		if type(fn) == 'function' then --XXX: We don't support callable tables; neither does "#invoke"?
			ret = {fn(frameSpoof)}
		end
		rawset(mw, mpkey, getCurrentFrame) -- Revert earlier monkey patch
		--TODO: if "ret" is "nil", some nice error about "'fnname' is not a function in 'modname'"
		return table.concat(ret)
	end
	return pkgapi
end