Extension:Scribunto

From Gamepedia Help Wiki
Jump to: navigation, search

Scribunto is an extension that allows scripting languages to be used within MediaWiki. This replaces the need to try to use wikitext as a scripting language, which results in poor performance and unreadable code.

Scribunto comes with Lua support by default, a fast and powerful language which is commonly seen as an embedded language in games, such as Garry's Mod and World of Warcraft.

Before starting with Lua in MediaWiki, it is best to have a basic understanding of Lua itself. The official Programming in Lua book is a good resource for learning Lua if you already have a programming mindset (already knowing a language like JavaScript is quite helpful, as there a many similarities), otherwise the tutorials at lua-users.org and Wikipedia's Lua for beginners help page may be useful.


This guide will cover the general usage of Scribunto, rather than using Lua itself. All examples will be saved at Module:Example and Template:Scribunto example (and their sub-pages), and examples after the first one won't repeat boilerplate code unless relevant. Use the reference manual for detailed usage of all functions.

Before getting started[edit | edit source]

If you want to use Scributo for the first time on your wiki, you have to have the Extension:Scribunto enabled on your wiki. You can check by looking for it on Special:Version on your wiki. If it's not listed, follow the instruction on Requesting extensions. While you have Scribunto added, make sure to also have enabled Extension:CodeEditor - you'll need it too.

Starting out[edit | edit source]

Scribunto stores scripts in the Module namespace. These modules can then be used on wiki pages by using the {{#invoke:}} parser function (referred to as "invoking" or "calling" the module). Scripts can not be directly written in wikitext by design, and it is recommended to call all modules from a template rather than using the {{#invoke:}} parser function on a page, to reduce wikitext clutter.

Scribunto scripts which are to be invoked must return a table which contains functions. The functions then return a wikitext string which is what is actually output to the page. Refer to this most basic working script:

local p = {}
p.helloWorld = function()
	return 'Hello, world!'
end
return p

Which would be called like this: {{#invoke: Example | helloWorld }}: Hello, world!

This script creates a table called p, adds a function called helloWorld to the table, and then returns the text Hello, world! which is displayed on the page.

Getting arguments[edit | edit source]

Now, this wouldn't be very useful without being able to send parameters to the script like you can with templates. Scribunto stores the parameters (which it calls "arguments", or "args" for short) in a "parser frame". This frame is basically a table which contains some useful functions related to the wikitext parser, as well as the table which contains the arguments. It is available as the first variable of the function in the script, and can also be retrieved with the mw.getCurrentFrame() function.

Direct arguments[edit | edit source]

Direct arguments or "normal" arguments are those which are set on the {{#invoke:}} parser function. Here's an example which uses arguments:

p.helloName = function( f )
	local args = f.args
	return 'Hello, ' .. args.name .. '!'
end

Which would be called like this: {{#invoke: Example | helloName | name = John Doe }}: Hello, John Doe!
Or in a template like this: {{#invoke: Example | helloName | name = {{{name|}}} }}, {{Scribunto example|name=John Doe}}: Hello, John Doe!

This script assigns the f variable to the frame, then retrieves the args table from the frame and assigns it to the args variable, and finally returns the text input in the name arg with Hello, and ! wrapped around it. Numbered or anonymous args (eg: {{{1}}}) are available too, as args[1].

The argument values are always strings. Like templates, named and numbered args have whitespace trimmed, whereas anonymous args do not; and arguments that are specified but with no value will be empty strings, rather than nil.

Parent arguments[edit | edit source]

With templates which use a sub-template, it is common to need to pass along args received from the parent template to the sub-template.

{{Template|{{{1}}}|arg1={{{arg1|}}}|arg2={{{arg2|}}}}}

Code like this can get rather messy, and can have performance issues as all the arguments will be parsed, regardless of if the template uses them.

Scribunto provides a way to access these "parent args" directly, without needing to manually pass them all through. This is very useful, as you will almost always be calling a script from a template, and it allows you to have an infinite number of possible arguments, something that wasn't possible with traditional templates.

To access the args of the parent template, you need the parent frame. Using the f:getParent() function on the current frame returns the parent template's frame, which can then be used in the same way as the current frame.

The concept of a "parent frame" may be difficult to grasp at first, so here's the previous example using it:

p.parentHello = function( f )
	local args = f:getParent().args
	return 'Hello, ' .. args.name .. '!'
end

Now, we can't just call this directly as we're no longer reading the current frame's args. Instead we will insert the {{#invoke:}} parser function in a template and use it from there. Note the lack of any template arguments being passed along.
{{#invoke: Example | parentHello }}, {{Scribunto example/Parent hello|name=John Doe}}: Hello, John Doe!
It works just like the previous example, despite the name arg not being manually passed to the {{#invoke:}} parser function.

For a single argument like this, it doesn't pose much of an improvement. But think about templates where many arguments are passed, like a navbox. Typically, these templates have a limit of the amount of rows they support, purely because that is as many args as have been manually set up. Using the parent frame directly would allow a navbox with no limit of rows, and would not need to check every row up to its limit to see if any of them have a value. Not only is this faster, but produces much better, and less repetitive code.

Supporting both[edit | edit source]

Using both direct args and parent args for separate purposes is fairly simple:

p.makeConfigGreeting = function( f )
	local args = f.args
	local parentArgs = f:getParent().args
	return args.greeting .. ', ' .. parentArgs.name .. '!'
end

The direct args are used as a "configuration" for the type of greeting to use, and the parent args are used to set who is being greeted.
{{#invoke: Example | makeConfigGreeting | greeting = Hello }}, {{Scribunto example/Config hello|name=John Doe}}: Hello, John Doe!
{{#invoke: Example | makeConfigGreeting | greeting = G'day }}, {{Scribunto example/Config g'day|name=John Doe}}: G'day, John Doe!
Here, there are two templates calling the same module, and are using its direct args to configure it to use different greetings. Then the templates are transcluded as usual and the parent args from the templates are used for the name of the person being greeted.


To have a module be able to use direct args or parent args, you just need to check if either table has any values:

p.makeFlexableGreeting = function( f )
	local args = f.args
	local parentArgs = f:getParent().args
	for _ in pairs( parentArgs ) do
		args = parentArgs
		break
	end
	
	return args.greeting .. ', ' .. args.name .. '!'
end

This module gets both the direct args and the parent args then starts a loop over the parent args. If the parent args table is empty, the code inside the loop won't run, and thus the direct args will remain assigned to the args variable. Otherwise, the args variable will be reassigned to the parent args table, and then the loop will be stopped as knowing the existence of a value is all that is necessary.


To have a module be able to use direct args and parent args, you could just do something like this:

p.makeFlexableConfigGreeting = function( f )
	local args = f.args
	local parentArgs = f:getParent().args
	
	local greeting = parentArgs.greeting or args.greeting
	local name = parentArgs.name or args.name
	return greeting .. ', ' .. name .. '!'
end

Which works okay for a simple example like this. However, this will be messy for modules with lots of args. The proper way to do it is to iterate over both tables and merge them into one:

p.makeMergedGreeting = function( f )
	local directArgs = f.args
	local parentArgs = f:getParent().args
	
	local args = {}
	for _, argType in ipairs{ directArgs, parentArgs } do
		for key, val in pairs( argType ) do
			args[key] = val
		end
	end
		
	return args.greeting .. ', ' .. args.name .. '!'
end

This module iterates over both arg tables, merging them together and overwriting the direct args with the parent args. This is useful for more complex configuration where a template sets default configuration settings, and then those settings can be overwritten as appropriate when transcluding the template.

Both of these examples could run into issues where "empty" parent arguments overwrite the direct arguments, as Lua considers empty strings to be "truthy" values. This could be fixed by trimming the whitespace from the values (mw.text.trim()), and then checking if they are equal to an empty string before setting the value.

Using wikitext[edit | edit source]

Scripts can output wikitext, just like a normal template, however only the final expansion stage of the parser will be run on this wikitext. This means that templates, parser functions, extension tags, and anything else that can output wikitext can not be used in the output. In order to use these, Scribunto provides some functions to expand them to their final wikitext. Other things that seem like they shouldn't work, such as variables ({{PAGENAME}}) and behaviour switches (__NOTOC__), do work because they do not output wikitext, and thus don't require extra processing.

f:preprocess()[edit | edit source]

This is most basic preprocessing function. This manually runs the preprocess stage of the parser on whatever text you provide it. You could theoretically run this on any wikitext before you return it to (almost) guarantee that all wikitext will work. However this isn't recommended, not just because of the performance implications of unnecessarily running the parser, and the fact that it is full wikitext and thus is prone to the same limitations as full wikitext, but because it is a rather brute force approach. You'll probably always want to use one of the more specialised functions below.

f:expandTemplate()[edit | edit source]

This function is faster and less error-prone than manually constructing a wikitext transclusion to use in the above function. You're probably familiar with templates such as {{!}}, which allow special wikitext characters to be ignored by the preprocessor. The f:expandTemplate() function is not subject to these limitations. Something like this would work fine:

f:expandTemplate{ title = 'Example', { arg1 = 'Pipe characters? | No problem! =)' }}

Whereas with a normal template transclusion you would have to do this:

{{Example|arg1=Pipe characters? {{!}} Need escaping! {{=}}(}}

Like its wikitext equivalent, this can transclude pages in any namespace (or the main namespace by prefixing the title with :).

f:callParserFunction()[edit | edit source]

Same deal as the previous function, but this one is for parser functions. Don't use this to call parser functions where there is a Lua equivalent, like {{urlencode:}} (mw.uri.encode()). The Lua equivalent will always be faster and more reliable.

f:extensionTag()[edit | edit source]

This one is for extension tags, such as <nowiki/> (but don't use it for that, use mw.text.nowiki()). This is pretty much an alias for f:callParserFunction() with the {{#tag}} parser function set, and the tag content prepended to the args.

Modular modules[edit | edit source]

Modules can be used in other modules using the require() function. Any global variables in the required module will be available globally, and the required module's return value will be returned by require.

Here's a simple example module which will be required, note the use of global variables.

name = 'John Doe'
constructHello = function( person )
	return 'Hello, ' .. person .. '!'
end

Now to require it:

p.acquireGlobals = function( f )
	require( 'Module:Example/AcquireGlobals' )
	return constructHello( name )
end

{{#invoke: Example | acquireGlobals }}: Hello, John Doe!

While this works, globals in general aren't recommended, and because the required module does not return a table of functions it can not be invoked directly, which makes it less useful and also difficult to debug. It is recommended to use local variables and return whatever variable you want requiring scripts to access, as this more flexable and makes debugging easier. Formatting scripts in a similar manner to scripts that will be invoked (returning a table of functions) is even better, as it is fairly simple to adjust the script to be able to be required and invoked.

This script is closer to the normal invoking style:

local p = {}
p.name = 'John Doe'
p.constructHello = function( person )
	return 'Hello, ' .. person .. '!'
end
return p
p.requiredHello = function( f )
	local helloModule = require( 'Module:Example/Hello' )
	local name = helloModule.name
	return helloModule.constructHello( name )
end

{{#invoke: Example | requiredHello }}: Hello, John Doe!


Here's a simple way to set up a module which can be required or called from templates:

local p = {}
p.constructHello = function( f )
	local args = f
	if f == mw.getCurrentFrame() then
		args = f:getParent().args
	else
		f = mw.getCurrentFrame()
	end
	
	return 'Hello, ' .. args.name .. '!'
end
return p

This starts out by saving whatever f contains under args. Then it checks if f is a frame, if it is, this means it's being invoked and so it gets the frame's parent args and saves them to args. Otherwise, it is being required from another module, and thus f contains the args, which is why it was assigned to args at the start. It is then reassigned to the current frame, so that it can be used for its useful functions, as if it had been invoked. If the module isn't using any of the frame's functions, then you could skip reassigning it.

This could then be called from a template in the usual way, or from a module like this:

p.requiredInvoke = function( f )
	local helloModule = require( 'Module:Example/FlexableHello' )
	return helloModule.constructHello{ name = 'John Doe' }
end

{{#invoke: Example | requiredInvoke }}: Hello, John Doe!
Note how the module is passing a table of values directly to the function of the module it is requiring.

Big data[edit | edit source]

It's likely at some point you're going to want to have a big table of data which is referenced by various scripts. Repeatedly parsing this big table, which is always going to be the same, hundreds of times for individual scripts on a single page is a waste of time and memory. Scribunto provides the mw.loadData() function exactly for this purpose. This function is similar to require(), except the module it requires must return a table containing only static data. No functions, no metatables. Any subsequent calls to mw.loadData() for the same module in any scripts on the page will return the already parsed table.

The table is read-only and trying to mw.clone() the table will result in the read-only flag being cloned as well. If you need to modify the table, you should either go back to require() or iterate over the table, building a new one from its values.

return {
	name = 'John Doe'
	-- ... and lots of other data
}
p.bigData = function( f )
	local data = mw.loadData( 'Module:Example/Data' )
	return 'Hello, ' .. data.name .. '!'
end

{{#invoke: Example | bigData }}: Hello, John Doe!

Debugging[edit | edit source]

Scribunto will actually prevent saving a module with syntax errors, unless the "Allow saving code with errors" option is checked (for work in progress modules). However, other errors will be allowed to save and thus need to be debugged.

Script error[edit | edit source]

Script error: The function "this won't work" does not exist.

When a module breaks in a page, it will output a script error like the one above. Clicking it will show the error message, and if it was able to at least partially execute, a backtrace to where the error occurred. The page will also be added to the pages with script errors tracking category.

Debug console[edit | edit source]

It's probably best to find errors before saving your changes, and for this purpose a debug console is provided underneath the edit area. Using the console isn't the most obvious to begin with, as it acts more like the module has been required, rather than invoked, so frame functions won't work unless you manually pass a frame to it.

For a module which doesn't use any frame functions, it is reasonably simple. Pass a table containing the args table under the name "args" to the function:

Scribunto console example frame args.png

And for a module which is actually already set up for being required it is even easier:

However a module that is not set up for being required and uses frame functions is a bit more difficult. You first have to get the frame, then set the args on the frame, then pass the frame to the function:

Scribunto console example frame.png

Notice how the variables are retained between multiple console commands. The state is only cleared when the module is changed (a message will be displayed indicating this), or when the clear button is pressed.

A module which only accepts parent args will have to be edited first to properly support requiring, or just comment out the original logic and assign the args to the frame:

Scribunto console example parent frame.png


Any calls to the mw.log() and mw.logObject() functions in the module will also be displayed in the console, before the return value, as long as the module doesn't encounter any errors.

Known issues and solutions[edit | edit source]

Lua error: Internal error: The interpreter has terminated with signal "24".[edit | edit source]

This problem occurs when the interpreter is running too long and is terminated. For example having an infinite loop in your program can cause this.

This error can also occur when long/resource intensive modules are run and the server the wiki is located on is on heavy load. A strategy to avoid this error is to improve the module so that it runs faster.

mw.text.split is very slow[edit | edit source]

If possible use the string library instead to split your springs using lua patters. For example:

function split(str, pattern)
    -- Splits string into a table
    --
    -- str: string to split
    -- pattern: pattern to use for splitting
    local out = {}
    local i = 1
    local split_start, split_end = string.find(str, pattern, i)
    while split_start do
        out[#out+1] = string.sub(str, i, split_start-1)
        i = split_end+1
        split_start, split_end = string.find(str, pattern, i)
    end
    out[#out+1] = string.sub(str, i)
    return out
end
Promotional Content