Module:Sandbox/Grufo
Appearance
This is an example module to demonstrate how a module can detect how it is loaded and why such can be useful.
- Module:Sandbox/Grufo {{key|}} {{nums}} {{strs}}
- Module:Sandbox/Grufo {{key|}} {{nums}} {{strs}}
- Module:Sandbox/Grufo {{key|'"`UNIQ--nowiki-00000000-QINU`"'}} {{nums}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=a|2=b|3=c}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|0=b|-1=a}} {{strs|+1=c}}
- Module:Sandbox/Grufo {{key|=}} {{nums}} {{strs|1.1=b|1e2=c|01=a}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b|-49=a}} {{strs|+49=c}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b|-99999999999999=a|99999999999999=c}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b|-100000000000000=a【T430893】|100000000000000=c【T430893】}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b|-9007199254740991=a【T430893】|9007199254740991=c【T430893】}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b|-9007199254740992=a【T430893】|9007199254740992=c【T430893】}} {{strs}}
- Module:Sandbox/Grufo {{key|=}} {{nums|1=b}} {{strs|-9007199254740993=a|9007199254740993=c}}
-- 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 parentFrame = currentFrame:getParent()
local type = type
local error, assert = error, assert
local pairs, ipairs = pairs, ipairs
local tostring, tonumber = tostring, tonumber
-- Is this a number that can be marshalled as an exact integer?
local is_exactint
do
--[=[XXX: IEEE 754 binary64 "double" implies 53-bit significand
math.ldexp(1, 53) == 9007199254740992 which is from:
[[d:Special:GoToLinkedPage/mediawikiwiki/Q117038765|LuaSandbox]]
see [[gitiles:mediawiki/php/luasandbox/+/refs/tags/4.1.3/data_conversion.c#213]]
]=]
local MAX_EXACTINT = math.ldexp(1, 53)
local MIN_EXACTINT = -MAX_EXACTINT
function is_exactint(n)
return 'number' == type(n)
and n >= MIN_EXACTINT and n <= MAX_EXACTINT
and n % 1 == 0
end
end
-- Make a sanitized copy of `args`
local function sanitize_args(args0)
local args = {} -- Make a sanitized copy; this would be tricky to do field by field on demand
for k, v in pairs(args0) do
local kt, vt = type(k), type(v)
if 'boolean' == vt then
v = v and '1' or '' -- `checkArgs` used by `newChild`, `callParserfunction`, etc. does this
elseif 'number' == vt then
v = tostring(v)
else
assert('string' == vt, 'invalid type ' .. vt .. ' for arg "' .. k .. '"')
end
if 'number' == kt then
if not is_exactint(k) then
k = tostring(k) -- Convert noninteger parameters to strings
end
elseif 'string' == kt then
k = k:match("^%s*(.-)%s*$") -- Trim string parameters
local n = tonumber(k)
if is_exactint(n) and ("%d"):format(n) == k then
k = n -- Convert integer parameters to numbers
end
else
error('arg keys must be strings or numbers, ' .. kt .. ' given', 0)
end
args[k] = v
end
do
local unnamed = {}
for i, v in ipairs(args) do
unnamed[i] = true -- Assume in-order numeric parameters are unnamed
end
for k, v in pairs(args) do
if not unnamed[k] then
args[k] = v:match("^%s*(.-)%s*$") -- Trim arguments of named parameters
end
end
end
return args
end
-- frame:newChild emulation
local function newChild(frame, title, args0, noemu)
local baseFrame = parentFrame or currentFrame -- No need to pass a frame with no parent
--[=[ Input validation:
Scribunto does a fair job of its own input validation and throws errors when we pass these on
but we spoof "args" to the target *raw*, so in order to not cause issues for the target
sanitize and validate everything.
]=]
assert('string' == type(title), 'Title name must a string')
--[=[mw.title.makeTitle(title)]=] -- XXX: handle illegal pagenames?
-- Make a sanitized copy; this would be tricky to do field by field on demand
local args = sanitize_args(args0)
if noemu then
--XXX: Nobody actually has a good use for this table--why is it forced upon the API?
return baseFrame:newChild({title=title, args=args})
end
local frameSpoof = {}
return frameSpoof
end
local pkgName = ... or currentFrame:getTitle()
local pkgAPI = {}
--[=[ Debug console tests: basic "smoke" tests to just ensure it is not entirely broken
=p.invoke('string', 'sub', {'xyzzy', 2, 4}, true)
=p.invoke('string', 'sub', {'xyzzy', 2, 4})
=p.invoke('string', 'sub', {'xyzzy', 0, 0}, true)
=p.invoke('string', 'sub', {'xyzzy', 0, 0})
=p.invoke('string', 'sub', {'xyzzy', 0, 0, no_category=true}, true)
=p.invoke('string', 'sub', {'xyzzy', 0, 0, no_category=true})
]=]
function pkgAPI.invoke(modname, fnname, modargs0, noemu)
local baseFrame = parentFrame or currentFrame -- No need to pass a frame with no parent
--[=[ Input validation:
Scribunto does a fair job of its own input validation and throws errors when we pass these on
but we spoof "modargs" to the target *raw*, so in order to not cause issues for the target
sanitize and validate everything.
]=]
assert('string' == type(modname), 'Module name must a string')
assert('string' == type(fnname), 'Function name must a string')
-- Make a sanitized copy; this would be tricky to do field by field on demand
local modargs = sanitize_args(modargs0)
if noemu then
table.insert(modargs, 1, fnname)
table.insert(modargs, 1, modname)
return baseFrame:callParserFunction('#invoke', modargs)
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
]=]
--XXX: mw.title.makeTitle('Module', modname); handle illegal pagenames
--XXX: mw.site.namespaces.Module.canonicalName .. ':' .. modname; localize namespace
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 lexical 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
`frameSpoof` but we can build redirector stub functions that punt calls to those, so long
as we ensure to always call them with the right frame reference. We do not want to create
a frame via `newChild` unless we have to so we pad out `frameSpoof` with redirector stub
functions that cache the `newChild` call as the upvalue `frameDelayed` and then replace
as much of the functionality as we can to minimize usage of the redirecting stub functions.
Of course the `args` field is not a function so it we must handle it differently but we
at first ignore that and later ensure to overwrite it with our local `args` spoofer. Of
course, we overwrite the redirector 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 = {}
local frameDelayed
-- 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, ...)
if not frameDelayed then
frameDelayed = baseFrame:newChild{title = modtitle, args = modargs}
end
if self == frameSpoof then
self = frameDelayed -- Only redirect correct value and let error propagate
end
return frameDelayed[name](self, ...)
end
end
-- Spoof `args` including recreating a metatable for read-only access to upvalues
local args_mt = {}
frameSpoof.args = setmetatable({}, args_mt)
function args_mt:__index(key)
local args = frameDelayed and frameDelayed.args or modargs
return args[key]
end
function args_mt:__pairs()
local args = frameDelayed and frameDelayed.args or modargs
local f0, s0, v0 = pairs(args)
-- Redirect so target can never get a direct reference to the args implementation
return function(s, v) return f0(s0, v) end, self, v0
end
--XXX: Because `ipairs` iterator does not respect `__index` effectively using `rawget`
function args_mt:__ipairs() --XXX: deprecated in Lua 5.3 and removed in 5.4
local args = frameDelayed and frameDelayed.args or modargs
local f0, s0, v0 = ipairs(args)
-- Redirect so target can never get a direct reference to the args implementation
return function(s, v) return f0(s0, v) end, self, v0
end
--XXX: At this point `frameSpoof` is technically complete but we spoof a few more
-- Spoof `getParent`
function frameSpoof:getParent(...)
if self ~= frameSpoof then
return baseFrame.getParent(self, ...) -- Let a real frame throw the error
end
return frameDelayed and frameDelayed:getParent(...) or baseFrame
end
-- Spoof `getTitle`
function frameSpoof:getTitle(...)
if self ~= frameSpoof then
return baseFrame.getTitle(self, ...) -- Let a real frame throw the error
end
return frameDelayed and frameDelayed:getTitle(...) or modtitle
end
-- Emulate the `#invoke` call with `require` and our `frameSpoof`
-- Save contents of `package.loaded`
local loaded = package.loaded
local pkgloaded = {}
for k, v in pairs(loaded) do
pkgloaded[k] = v
end
loaded[modtitle] = nil -- Force `require` to load from scratch
local getCurrentFrame = mw.getCurrentFrame
-- Monkey patch `getCurrentFrame` to return our `frameSpoof`
function mw.getCurrentFrame()
return frameSpoof
end
-- `require` here so the target initialization block sees our monkey patched `getCurrentFrame`
local mod = require(modtitle) --TODO: Wrap with `pcall` to catch `require` errors?
loaded[modtitle] = pkgloaded[modtitle] -- Fixup for `require`
local fn, results
if 'table' == type(mod) then -- Only handle table returns from `require`
fn = mod[fnname]
end
if 'function' == type(fn) then --XXX: We don't support callable tables; neither does `#invoke`?
results = { fn(frameSpoof) } -- Support multiple return values as sequence; same as `#invoke`
end
mw.getCurrentFrame = getCurrentFrame -- Revert earlier monkey patch
-- Restore `package.loaded` and pretend `require` and `fn` were never called here
for k in pairs(loaded) do
if nil == pkgloaded[k] then
loaded[k] = nil -- Delete anything new
end
end
for k, v in pairs(pkgloaded) do
loaded[k] = v -- Restore everthing else
end
--TODO: if `'table' ~= type(mod)`, throw an error about "`modtitle` is not a table package"
--TODO: emulate `scribunto-common-nosuchfunction` and throw an error about "`fnname` is not a key/field in `modtitle`" if `nil == fn`
--TODO: emulate `scribunto-common-notafunction` and throw an error about "`fnname` is not a function in `modtitle`" if `nil == results`
local stringResults = {}
for i, result in ipairs(results) do
stringResults[i] = tostring(result)
end
return table.concat(stringResults)
end
function pkgAPI.main(args, key)
local keyarg = '{{key|' .. mw.text.nowiki(key) .. '}}'
local numargs, strargs = '{{nums', '{{strs'
for param, arg in pairs(args) do
local paramtype = type(param)
if 'number' == paramtype then
numargs = numargs .. '|' .. ("%d"):format(param) .. '=' .. mw.text.nowiki(arg)
if args[param] ~= arg then
if args[("%d"):format(param)] ~= arg then
numargs = numargs .. '【BROKENPARAM】'
else
numargs = numargs .. '【[[phab:T430893|T430893]]】'
end
end
elseif 'string' == paramtype then
strargs = strargs .. '|' .. mw.text.nowiki(param) .. '=' .. mw.text.nowiki(arg)
if args[param] ~= arg then
strargs = strargs .. '【BROKENPARAM】'
end
end
end
numargs, strargs = numargs .. '}}', strargs .. '}}'
return pkgName, ' ', keyarg, ' ', numargs, ' ', strargs, nil, ' ', pkgName
end
--XXX: `mw.isSubsting` for pre-save transform substitution
--XXX: `currentFrame:preprocess('{{REVISIONID}}') == ''` for preview
--XXX: `parentFrame:preprocess('<noinclude>1</noinclude>') == ''` for non-source page transclusion
if ... then
-- `require and `mw.loadData`
if not parentFrame then
--[=[ `mw.loadData`
This is where data that is static during a page rendering should be made available to modules
]=]
return {currentFrame:getTitle(), 'loadData', pkgName}
end
--[=[ `require`
This is where "main" functionality should be made available to modules
]=]
return pkgAPI
else
-- `#invoke` and console
if not parentFrame then
--[=[ console
This is where debug functionality should be made available to the console as `p`
]=]
return pkgAPI
end
--[=[ `#invoke`
This is where "wrapper" functionality should be made available to wikitext templates, etc.
]=]
local mt = {}
function mt:__index(key)
return function(frame) --[=[
This is the function from `{{#invoke:module|key}}`, where `key` is any valid string argument
`#invoke` parses this first "function" argument differently (e.g., `=` is valid, etc.)
and sends the rest to the frame parser
Notice: `{{#invoke:module|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)
end