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 p

Save 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 p

Usage, 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 in debug.log if $wgDebugLogFile is 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 p

Now {{#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
end

When 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 #vardefine variable 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 p at the end of the file; the engine then returns nil and 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.ustring functions 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 p

Now 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 io or os libraries.

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
end

This 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 p

Now 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.

Subscribe to MediaWiki Tips and Tricks

Don’t miss out on the latest articles. Sign up now to get access to the library of members-only articles.
jamie@example.com
Subscribe