This page looks best with JavaScript enabled

Multi-Instance Subtemplates in Lua

 ·  ☕ 8 min read

Introduction

You’ve probably seen something like the following:

{{MatchRecapS8 |gamename=Game 1 /* snip */
|blue1={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue2={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue3={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue4={{MatchRecapS8/Player|id= |champion= /* snip */}}

Each {{MatchRecapS8/Player}} template will do some formatting like print a table row and maybe update some global variables along its way, and {{MatchRecapS8}} will insert those table rows in the correct place in the table, do some other stuff with its “direct” args, and print the overall result.

In wikitext, this is a very standard “design pattern” of sorts, and there’s no real reason to disrupt it - even if you were capable of sending each child template parameter individually to the parent, wikitext’s support for loops and arrays isn’t solid enough for your resulting awkward syntax to be worth it.

All this changes when you start using Lua, however - once your code is in Lua, you absolutely want all of your child arguments available to you directly for arbitrary computations, and a system like the above would be crippling compared to what the language supports. So how can we send individual child template arguments to the body of our code?

In other words, what we have right now is siloed individual templates that each has its own |id=, |champion=, etc, and are unable to communicate with each other. These individual parameters of |id= etc are also unavailable to the parent template. What we want, and what becomes practical to arrive at in Lua, is a setup in which there is only a single context in which we have a list of ids and a list of champions, etc, so that we can do any computations and displays needed.

In this article I’ll go through a couple different possible approaches to the issue of making these parameters available to the “parent” (I use this term very informally), including code examples from Leaguepedia.

(Note: All code in this post was originally written by me for Leaguepedia and is licensed under CC BY-SA 3.0.)

A mediocre solution - numbered arguments

One option is to eliminate the child templates completely and use a system like the following:

{{TeamRoster|team=Team Liquid|footnoteteamn=3
|player1= Quas|flag1= ve |role1=top
|player2= IWDominate|flag2= us |role2=jungle
|player3= FeniX|flag3= kr |role3=mid
|player4= KEITH|flag4= us |role4=ad
|player5= Piglet|flag5= kr |role5=ad
|player6= Xpecial|flag6= us |role6=support
|player7=Peter |link7= Peter (Peter Zhang)|flag7=cn |role7=coach
}}

In Lua, you can then do something like the following to retrieve args:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local PLAYER_FIELD_LIST = { 'Name', 'Link', 'Flag', 'Role', 'FootnoteN' }

-- snip

function h.getPlayerData(args)
	local i = 1
	local playerData = {}
	while i <= util_game.players_per_team or args[i] do
		playerData[i] = args[i] and util_args.splitArgs(args[i], PLAYER_FIELD_LIST) or {}
		h.castPlayerRow(playerData[i], i)
		i = i + 1
	end
	h.cropPlayerResults(playerData)
	h.addTeamDataToPlayers(playerData, args)
	return playerData
end

There’s a pretty big UX problem with this, though - what if we wanted to insert a second mid laner after FeniX? Then we need to move KEITH, Piglet, Xpecial, and Peter down - which means re-numbering at least twelve args by hand - not a pleasant experience without automated tooling, which is unlikely to exist for typical editors making typical edits to typical pages.

So while our code is pretty happy about the current state of affairs, our users most certainly are not. Let’s do better.

Serializing our child templates

The solution is to go back to child templates, serialize our inputs as strings, and then deserialize to tables in Lua. Going back to our {{MatchRecapS8/Player}} example above, here’s the code at that template (ignore {{Pentakills/CargoAttach}}):

<includeonly>{{{champion|}}};;;{{{link|}}};;;{{{name|}}};;;{{{summonerspell1|}}},{{{summonerspell2|}}};;;{{{item1|}}},{{{item2|}}},{{{item3|}}},{{{item4|}}},{{{item5|}}},{{{item6|}}};;;{{{trinket|}}};;;{{{kills|}}};;;{{{deaths|}}};;;{{{assists|}}};;;{{{gold|}}};;;{{{cs|}}};;;{{{keystone|}}};;;{{{secondary|}}};;;{{{pentakills|}}};;;{{{pentakillvod|}}};;;{{{nocargo|}}};;;{{{skillletter|}}};;;{{{skillimage|}}}{{Pentakills/CargoAttach}}</includeonly><noinclude>{{documentation|cargodec=
{{attach|ScoreboardPlayers}}
}}[[Category:Scoreboard Templates]]</noinclude>

Now we have a utility function called util_args.splitArgs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function h.splitArgs(input, fieldlist, sep)
	if not input or input == '' then return end
	sep = (sep and ('%s*' .. sep .. '%s*')) or '%s*;;;%s*'
	local result = {}
	local inputTbl = util_text.split(input,sep)
	for i, v in ipairs(fieldlist) do
		if not inputTbl[i] then
			error(('Missing parameter %s - maybe wrong child template?'):format(v))
		end
		if inputTbl[i] ~= '' then
			result[v] = inputTbl[i]
		end
	end
	return result
end

And in Module:ScoreboardAbstract we have:

1
2
3
4
5
6
7
8
9
local PLAYER_ARGS = { 'Champion', 'Link', 'Name', 'SummonerSpells', 'Items', 'Trinket', 'Kills', 'Deaths', 'Assists', 'Gold', 'CS', 'Keystone', 'Secondary', 'Pentakills', 'PentakillVod', 'nocargo', 'SkillLetter', 'SkillImage' }
-- snip
function h.splitPlayerDataAndErrorcheck(args, side, i, j)
	if not args[side .. j] then
		error(i18n.print('no_player_data', i, j))
	end
	local row = util_args.splitArgs(args[side .. j], PLAYER_ARGS)
	return row
end

You can see the similarity in approach to the Module:TeamRoster example from the introduction - we have an array of args that we want to extract, in the order that we expect them in, and we’re going to generate a row in an array of child args. But whereas before we relied on individually numbering every single child arg, now we only have to number the entire child arg row one time. A huge improvement!

The called wikicode will look identical to that in the first example, once again:

{{MatchRecapS8 |gamename=Game 1 /* snip */
|blue1={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue2={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue3={{MatchRecapS8/Player|id= |champion= /* snip */}}
|blue4={{MatchRecapS8/Player|id= |champion= /* snip */}}

Note that my choice of ;;; as the splitter is pretty arbitrary. You can use anything you want, as long as you’re really, really confident it’ll never come up in any actual string that you’re processing. Also, in the interest of performance, it probably shoudln’t contain any unicode characters, so that you can avoid using the ustring library for your split.

Going one step further - no numbered args at all

In some situations we may consider it appropriate to forego naming or numbering of child templates altogether.

For example, consider this input:

|pre={{RCPlayer|player=Johnsun |role=bot |status= |trainee=yes}}
{{RCPlayer|player=Grig |role=j |status= |move_type=to_main}}
{{RCPlayer|player=Fanatiik |role=j |status= |move_type=confirm}}
|post={{RCPlayer|player=Spica |role=j |status= |move_type=confirm}}
{{RCPlayer|player=Johnsun |role=bot |status= |sub=yes}}

Here we have a list of three players as pre and two players as post, but they arent really player1, player2, and player3, they’re just, three players.

In this case, the <includeonly> portion of my {{RCPlayer}} template looks like this:

{{{player|}}};;;{{{role|}}};;;{{{status|}}};;;{{{loaned_from|}}};;;{{{loaned_to|}}};;;{{{move_type|}}};;;{{{custom|}}};;;{{{contract_until|}}};;;{{{assistance|}}};;;{{{event|}}};;;{{{replacing|}}};;;{{{reason|}}};;;{{{phase|}}};;;{{{sub|}}};;;{{{trainee|}}};;;{{{rejoin|}}};;;{{{order|}}};;;{{{sentence_group|}}};;;{{{leave_date|}}};;;{{{nolink|}}};;;{{{remain_for|}}};;;{{{remain_for_link|}}};;;{{{already_joined|}}};;;{{#or:{{{team_priority|}}}|1}};;;{{{sister_team|}}};;;{{{reserve|}}};;;{{{changed_on_team_rename|}}}:::

Note the ::: at the end.

And I have a different Lua function for it:

1
2
3
4
5
6
function p.splitArgsArray(input, fieldlist, outersep, innersep)
	if not input or input == '' then return {} end
	outersep = outersep or '%s*:::%s*'
	local ret = util_map.split(input, outersep, h.splitArgs, fieldlist, innersep)
	return ret
end

Here, h.splitArgs references the same function as it does above; actually, now I should confess that the function I showed in the section above is actually private (as you might have guessed from its inclusion in the table h rather than p). The public version is:

1
2
3
4
5
6
7
function p.splitArgs(input, fieldlist, sep, outersep)
	-- outersep 99.9% of the time is going to be nil here, but on the off chance
	-- that we have to specify it, it's important to make it available
	-- so yeah it's shitty that we're switching arg order
	-- but blame lua for not having named params zzzzzzzzzzzz
	return p.splitArgsArray(input, fieldlist, outersep, sep)[1]
end

The reason for this wrapper has to do with how empty cases are handled; it was causing a problem when a public splitArgsArray was calling a public splitArgs, though to be honest I can’t remember precisely what the problem was, so now the public dependency is in the other direction.

For completeness, here’s the entire section of code from Module:ArgsUtil, though I have previously shown all three of these functions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function p.splitArgs(input, fieldlist, sep)
	return p.splitArgsArray(input, fieldlist, nil, sep)[1]
end

function p.splitArgsArray(input, fieldlist, outersep, innersep)
	if not input or input == '' then return {} end
	outersep = outersep or '%s*:::%s*'
	local ret = util_map.split(input, outersep, h.splitArgs, fieldlist, innersep)
	return ret
end

function h.splitArgs(input, fieldlist, sep)
	if not input or input == '' then return end
	sep = (sep and ('%s*' .. sep .. '%s*')) or '%s*;;;%s*'
	local result = {}
	local inputTbl = util_text.split(input,sep)
	for i, v in ipairs(fieldlist) do
		if not inputTbl[i] then
			error(('Missing parameter %s - maybe wrong child template?'):format(v))
		end
		if inputTbl[i] ~= '' then
			result[v] = inputTbl[i]
		end
	end
	return result
end

And here’s util_map.split:

1
2
3
4
5
function p.split(str, sep, f, ...)
	local tbl = util_text.split(str,sep)
	if not f then return tbl end
	return p.inPlace(tbl, f, ...)
end

Here’s how util_args.splitArgsArray is called:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function p.getPlayersFromArg(arg)
	local players = util_args.splitArgsArray(arg, PLAYER_ARG_PARTS)
	if not next(players) or not next(players[1]) then return OD() end
	return h.getPlayersGuaranteed(arg, players)
end

function h.getPlayersGuaranteed(arg, players)
	local ret = OD()
	for _, playerData in ipairs(players) do
		h.addPlayerData(ret, playerData)
	end
	return ret
end

(OD stands for OrderedDictionary and is my implementation of an Ordered Dictionary data type in Lua.)

Once again, my choice of ::: to divide individual players / instances of the child template from each other is pretty arbitrary; just pick something that will be globally unique. But if you want to follow my conventions, then ;;; divides individual arguments, and ::: divides instances of multi-instance child templates.

Conclusion

A common design pattern when working with Lua is to deliver multi-instance child-template data from MediaWiki markup to Lua by first serializing the user input and then deserializing in Lua. To do this, I use ;;; as my parameter separator, and, if applicable, I will separate instances of templates with :::.

Links to example templates and sample code (though remember all of this is subject to change):

Share on

river
WRITTEN BY
River
River is a developer most at home in MediaWiki and known for building Leaguepedia. She likes cats.


What's on this Page