Mastering Scribunto: Lua Scripting in MediaWiki
What Scribunto Actually Does
At its core Scribunto is the bridge that lets a MediaWiki install run Lua code inside the parser. The extension ships with a lightweight sandbox (either LuaStandalone or LuaSandbox) and gives you the #invoke parser function. When a template writes {{#invoke:MyModule|myFunction|arg1|arg2}} the wiki hands the call over to the Lua engine, which executes the function in Module:MyModule and returns the resulting string.
Why Use Lua?
- Lua runs about ten times faster than PHP for pure computation.
- Its table‑centric data model matches the way wiki data is usually handled.
- It keeps template logic out of the wikitext, making pages easier to read.
Getting a Module on the Page
A minimal module looks like this:
local p = {}
function p.hello( frame )
return "Hello, world!"
end
return pSave it as Module:Hello. Then in any article you can write:
{{#invoke:Hello|hello}}
and the output will be “Hello, world!”. That’s the classic “Hello, world!” of Scribunto – a good sanity check that the extension is installed correctly.
Passing Arguments
Modules often need data from the template that called them. The frame object is your portal to those arguments.
local p = {}
function p.greet( frame )
local name = frame.args[1] or "anonymous"
return "Greetings, " .. name .. "!"
end
return pUsage, for instance:
{{#invoke:Hello|greet|Ada Lovelace}}
produces “Greetings, Ada Lovelace!”. Note that frame.args[1] is the first positional argument, whereas frame.args["key"] can fetch named parameters.
The mw Library – Your Toolbox
MediaWiki exposes a handful of Lua libraries under the global mw table. A few that get used daily:
mw.title.new( titleString )– create a title object, then call:fullText(),:namespace()and friends.mw.uri.encode( str )– URL‑escape a string.mw.text.jsonEncode( table )/mw.text.jsonDecode( json )– round‑trip JSON, handy for passing structured data through templates.mw.html.create( "tag" )– build safe HTML fragments.mw.log( "debug message" )– write to the MediaWiki debug log (visible indebug.logif$wgDebugLogFileis set).
All these APIs are deliberately safe. The sandbox blocks file‑system access, network sockets, and any attempt to touch the global Lua environment outside the provided modules.
Example: Linking to a Page Dynamically
local p = {}
function p.link( frame )
local target = frame.args[1] or "Main_Page"
local title = mw.title.new( target )
if not title then
return "Invalid title"
end
return "[[" .. title.fullText .. "]]"
end
return pNow {{#invoke:Hello|link|Special:WhatLinksHere}} renders a link to the special page. The title.fullText property automatically includes the namespace prefix if needed.
Debugging Inside the Sandbox
When you hit an error, MediaWiki shows a fairly generic “Lua error” message. To get more insight, sprinkle mw.log calls in your code and enable $wgDebugLogFile in LocalSettings.php. For quick checks, you can also use mw.message.error() to surface a message directly on the page (only visible to users with edit rights).
function p.bad( frame )
local x = frame.args[1]
mw.log( "Received arg: " .. tostring( x ) )
if not x then
error( "Missing argument" )
end
return x
endWhen the module throws error("Missing argument"), the parser will output a red‑highlighted warning, and the log entry ends up in the debug file.
Performance Tweaks You Should Know
Even though Lua is fast, poorly written modules can still slow a page down, especially if they run a heavy loop for every view. MediaWiki has a built‑in threshold:
$wgScribuntoSlowFunctionThreshold = 0.02; // seconds
If a function exceeds the limit, MediaWiki records the timing in the profiling logs. Use this as a cue to refactor expensive code – maybe cache the result in a page property (mw.title.getContent()) or move the calculation to a separate module that only runs on a cron job.
Caching Strategies
mw.title.getContent()can be stored in a#vardefinevariable that persists across page renders.- For data that changes rarely (e.g., a list of country codes), consider a JSON file stored in
MediaWiki:ModuleData/...and read it once per request. - Never rely on global Lua tables to survive between requests – the sandbox is re‑initialized each time.
Configuration Essentials
Most wikis run the default Lua engine, but you can swap it out in LocalSettings.php:
$wgScribuntoDefaultEngine = "luastandalone"; // or "luasandbox"
$wgScribuntoEngineConf = [
"luastandalone" => [
"luaPath" => "/usr/bin/lua",
// optional timeout, memory limit, etc.
],
"luasandbox" => [
// no external binary required; runs purely in PHP
]
];If your host disallows executing binaries, the luasandbox implementation is the safe fallback. It’s a bit slower, but still plenty quick for typical template work.
Testing & Sandboxing
Every module gets a built‑in sandbox subpage (e.g., Module:Hello/sandbox). Open it, edit freely, and hit “Preview”. The sandbox runs the same environment as live modules, so you can experiment without breaking anything on the main namespace. Likewise, you can add a testcases subpage that contains {{#invoke:Hello|test}} calls – the output can be compared against expected values during code reviews.
Common Pitfalls
- Forgetting to
return pat the end of the file; the engine then returnsniland you get a blank output. - Using
mw.title.new()with an empty string – it throws an error that looks like “Bad argument #1 to ‘new’ (string expected)”. - Relying on
mw.ustringfunctions for Unicode when the input is already a Lua string; most of the time the plain string methods work fine.
Advanced Patterns
When you need to expose a collection of related helpers, package them under a single table and export multiple entry points.
local p = {}
local helpers = {}
function helpers.formatDate( iso )
-- iso: “2023‑04‑01T12:00:00Z”
local y, m, d = iso:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)")
return string.format("%s/%s/%s", d, m, y)
end
function p.show( frame )
local iso = frame.args[1] or os.date("!%Y-%m-%dT%H:%M:%SZ")
return helpers.formatDate( iso )
end
p.helpers = helpers
return pNow other modules can local m = mw.loadData( "Module:Hello" ) and call m.helpers.formatDate(...) without exposing the formatter as a public function. This pattern helps keep the public API tidy while still sharing code internally.
Security Considerations
Scribunto runs inside a sandbox that blocks:
- File system writes.
- Network sockets.
- Access to
iooroslibraries.
Nevertheless, never trust user‑supplied data blindly. Always sanitize strings before feeding them into mw.title.new or mw.uri.encode. A common safety net is:
local function safeTitle( title )
local t = mw.title.new( title )
if t and t.isLocal then
return t.fullText
else
return nil
end
endThis ensures the title resolves to a page in the local wiki, preventing cross‑wiki injection attacks.
Putting It All Together – A Mini‑Project
Suppose you want a template that lists the top‑N most‑viewed pages from a category. The heavy lifting lives in a module, the template just calls it.
-- Module:TopViews
local p = {}
-- Fetches page titles from the database (uses mw.ext.titleBlacklist for demo)
function p.list( frame )
local cat = frame.args[1] or "Category:Featured"
local n = tonumber( frame.args[2] ) or 5
local sql = string.format(
"SELECT page_title FROM categorylinks "..
"JOIN page ON cl_from = page_id "..
"WHERE cl_to = %s "..
"ORDER BY page_counter DESC LIMIT %d",
mw.title.makeTitle(0, cat):inNamespace(14):escaped(),
n
)
local res = mw.sql.query( sql )
local out = {}
for _, row in ipairs( res ) do
table.insert( out, "[[" .. mw.title.makeTitle(0, row.page_title):fullText .. "]]" )
end
return table.concat( out, ", " )
end
return pNow a template can simply do:
{{#invoke:TopViews|list|Category:Technology|10}}
The result is a comma‑separated list of the ten most‑viewed pages in the “Technology” category. Because the module runs a SQL query, you might want to add a cache layer with mw.cache.get() and mw.cache.set() – but that’s a story for another day.
Final Thoughts
Scribunto has been part of MediaWiki for years, yet many wikis still cling to giant wikitext templates that could be expressed in a dozen lines of Lua. The payoff is speed, readability, and a more maintainable code base. Remember to test in the sandbox, keep an eye on the slow‑function threshold, and lean on the mw library for safe operations. With those habits, mastering Lua scripting on MediaWiki becomes less of a mystic art and more of a regular part of your editing workflow.