Using Lua Modules in MediaWiki with the Scribunto Extension

Using Lua Modules in MediaWiki with the Scribunto Extension

Ever stared at a wikitext page that just wouldn’t behave the way you wanted, and thought “there must be a better way”? You’re not alone. In the wild world of MediaWiki the answer often lives in the Module namespace, powered by the Scribunto extension. Below is a compact guide that cuts straight to the chase, yet tries not to sound like a robot’s instruction manual.

What Scribunto actually does

Scribunto injects a full‑blown Lua interpreter into MediaWiki. You write Lua scripts (called modules) inside pages that live under Module:. When a normal wiki page includes #invoke, Scribunto spins up the interpreter, runs the code, and spits the result back into the HTML stream. The heavy lifting happens on the server, so the reader never sees the Lua – just the rendered output.

Key points:

  • The module lives in its own namespace – Module:MyExample.
  • Calls are made via the parser function #invoke.
  • All Lua runs in a sandbox; only the mw library is exposed.

Getting your hands dirty: a minimal module

Let’s start simple. Create a page called Module:HelloWorld and drop the following into it:


-- Simple greeting module
local p = {}
function p.greet(frame)
    local name = frame.args[1] or "world"
    return "Hello, " .. name .. "!"
end
return p

This little snippet defines a table p, adds a function greet that grabs the first argument, and returns a friendly string. Notice the “or” fallback – a tiny bit of defensive programming that saves you from a nil surprise.

Calling the module from wikitext

Now sprinkle the following on any article page:


{{#invoke:HelloWorld|greet|Alice}}

When the page renders you’ll see:

Hello, Alice!

If you leave out the argument, the default kicks in and you get “Hello, world!”. Handy for quick prototypes, but the real power lies in the mw object.

The mw library – your Swiss‑army knife

All modules get a global table called mw. It bundles a handful of sub‑libraries that let you interact with the wiki’s data model without opening the entire codebase. A few favorites:

  • mw.title – work with page titles, namespaces, and redirects.
  • mw.text – manipulate wikitext, strip HTML, or expand templates.
  • mw.html – safely construct HTML fragments.
  • mw.site – fetch site‑wide settings or language information.
  • mw.message – get localized interface messages.

Example using mw.title and mw.html to build a tiny navigation box:


local p = {}
function p.navbox(frame)
    local cur = mw.title.getCurrentTitle()
    local ns = cur.namespace
    local list = {}
    for i = 1, 5 do
        local t = mw.title.new(ns, "Page" .. i)
        table.insert(list, mw.html.createLink(t.fullText, t.fullText))
    end
    return table.concat(list, " | ")
end
return p

When you invoke #invoke:HelloWorld|navbox on a page in the main namespace, you’ll see a series of links to “Page1” … “Page5”. The code might look a little dense, but each line does something obvious once you get the hang of the mw helpers.

Debugging inside the sandbox

Because Lua runs on the server, you don’t have a Chrome console to poke at. The usual trick is to use mw.log – it writes to the PHP error log, which you can then tail. Example:


mw.log("Debug: title=" .. cur.fullText)

Or, for quick-and-dirty feedback on a page, return a string that clearly marks the debug output. It’s a bit clunky, but it works:


return ""

Remember to strip out those debug comments before you push the code to production – they’re harmless to the user but can clutter the source.

Performance tips you probably didn’t think about

Lua is fast, but a badly written module can still throttle page loads:

  1. Cache heavy computations. Use mw.loadData to fetch static tables from JSON files stored in Module: pages. Once loaded, the data is cached per request.
  2. Avoid deep recursion. The sandbox caps recursion depth to protect the server. If you need to walk a tree, prefer an explicit stack.
  3. Don’t over‑fetch. Each call to mw.title.new may hit the database. Batch operations whenever you can.
  4. Mind template expansion. Calling mw.text.expandTemplate inside a loop can explode in cost. Cache the result outside the loop if the template is static.

Here’s a tiny snippet that demonstrates caching JSON data:


local data = mw.loadData("Module:CountryCodes")
function p.getCode(frame)
    local country = frame.args[1] or ""
    return data[country] or "??"
end
return p

The CountryCodes module would contain a Lua table mapping country names to ISO codes. The first time the module runs, MediaWiki loads and parses the JSON; subsequent calls reuse the same table, shaving off precious milliseconds.

Security – why the sandbox matters

Scribunto’s sandbox isn’t just a fancy word. It blocks:

  • Access to the file system – no reading or writing arbitrary files.
  • Network sockets – you can’t open HTTP connections directly.
  • Global variables outside mw – the environment is heavily trimmed.

This means you can safely expose logic to the public without fearing that a mischievous user will turn the interpreter into a back‑door. Still, avoid loadstring or load with unchecked input; they can re‑introduce code injection vectors.

Learning resources – where to turn when you’re stuck

The official documentation lives on MediaWiki’s site. Two pages in particular are worth bookmarking:

Stack Overflow has a few threads about importing modules in external scripts, but for on‑wiki work you’ll rarely need to go outside the mw sandbox.

Common pitfalls (and how to avoid them)

1. Forgetting to return the module table. If you end a module with just p instead of return p, the interpreter treats it as nil and you’ll see “Module call failed”.

2. Mismatched argument names. #invoke:MyMod|func|arg1=foo passes foo under frame.args[“arg1”], not frame.args[1]. It’s easy to mix the two up, especially when you start mixing positional and named arguments.

3. Using reserved words as table keys. Lua forbids keys like and or or without quoting them. A quick ["and"] = … fixes the issue.

4. Assuming mw.title.getCurrentTitle() always returns a title object. In special pages (e.g., Special:UserLogin) it may be nil. Guard against that with a simple check.

Putting it all together – a real‑world example

Imagine you want a template that lists the top‑scoring players from a game, pulling data from a JSON module. Here’s a sketch:


-- Module:TopPlayers
local data = mw.loadData("Module:GameScores")  -- { player = score, … }

local function sortedPairs(t)
    local keys = {}
    for k in pairs(t) do table.insert(keys, k) end
    table.sort(keys, function(a, b) return t[a] > t[b] end)
    local i = 0
    return function()
        i = i + 1
        if keys[i] then return keys[i], t[keys[i]] end
    end
end

local p = {}
function p.list(frame)
    local limit = tonumber(frame.args[1]) or 10
    local out = {}
    local count = 0
    for player, score in sortedPairs(data) do
        count = count + 1
        if count > limit then break end
        table.insert(out, mw.html.createLink(player, player) .. ": " .. score)
    end
    return table.concat(out, "")
end

return p

You can then embed the list on any page with:


{{#invoke:TopPlayers|list|5}}

The result is a neat HTML‑formatted ranking, updated automatically whenever the GameScores JSON module changes. No need for a separate backend service – the wiki itself does the heavy lifting.

Final thoughts

Lua modules via Scribunto give MediaWiki a programmable edge that feels almost like a tiny app platform. The learning curve isn’t steep – you write a few lines of Lua, sprinkle #invoke, and watch the pages come alive. At the same time, the sandbox, built‑in mw library, and caching primitives keep you from stumbling into performance or security pitfalls.

So whether you’re sprucing up a template, building a dynamic navigation widget, or exposing complex calculations to editors, the path is clear: write a Module, call it with #invoke, and let Scribunto handle the rest. The rest of the wiki will thank you – and maybe, just maybe, you’ll find yourself reaching for Lua a little more often.

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