<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://nssportwiki.com/index.php?action=history&amp;feed=atom&amp;title=Module%3AFootball_event</id>
	<title>Module:Football event - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://nssportwiki.com/index.php?action=history&amp;feed=atom&amp;title=Module%3AFootball_event"/>
	<link rel="alternate" type="text/html" href="https://nssportwiki.com/index.php?title=Module:Football_event&amp;action=history"/>
	<updated>2026-07-10T20:49:17Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.40.1</generator>
	<entry>
		<id>https://nssportwiki.com/index.php?title=Module:Football_event&amp;diff=76025&amp;oldid=prev</id>
		<title>Sel Appa: Created page with &quot;-- Module:Football event -- Implements the association-football match-event icon templates and provides the -- goal / penalty auto-formatting for Module:Football box. -- -- Template entry points: --   {{#invoke:Football event|goal}}        {{#invoke:Football event|goldengoal}} --   {{#invoke:Football event|yellow}} / yel {{#invoke:Football event|sentoff}} --   {{#invoke:Football event|subon}}        {{#invoke:Football event|suboff}} -- Module entry point (called from...&quot;</title>
		<link rel="alternate" type="text/html" href="https://nssportwiki.com/index.php?title=Module:Football_event&amp;diff=76025&amp;oldid=prev"/>
		<updated>2026-07-10T04:01:15Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;-- Module:Football event -- Implements the association-football match-event icon templates and provides the -- goal / penalty auto-formatting for &lt;a href=&quot;/index.php?title=Module:Football_box&quot; title=&quot;Module:Football box&quot;&gt;Module:Football box&lt;/a&gt;. -- -- Template entry points: --   {{#invoke:Football event|goal}}        {{#invoke:Football event|goldengoal}} --   {{#invoke:Football event|yellow}} / yel {{#invoke:Football event|sentoff}} --   {{#invoke:Football event|subon}}        {{#invoke:Football event|suboff}} -- Module entry point (called from...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;-- Module:Football event&lt;br /&gt;
-- Implements the association-football match-event icon templates and provides the&lt;br /&gt;
-- goal / penalty auto-formatting for [[Module:Football box]].&lt;br /&gt;
--&lt;br /&gt;
-- Template entry points:&lt;br /&gt;
--   {{#invoke:Football event|goal}}        {{#invoke:Football event|goldengoal}}&lt;br /&gt;
--   {{#invoke:Football event|yellow}} / yel {{#invoke:Football event|sentoff}}&lt;br /&gt;
--   {{#invoke:Football event|subon}}        {{#invoke:Football event|suboff}}&lt;br /&gt;
-- Module entry point (called from another module):&lt;br /&gt;
--   require(&amp;#039;Module:Football event&amp;#039;).formatBox{&lt;br /&gt;
--       goals1 = ..., goals2 = ..., penalties1 = ..., penalties2 = ... }&lt;br /&gt;
&lt;br /&gt;
local p = {}&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- shared data&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local OG_LINK  = &amp;#039;([[Own goal#Association football|o.g.]])&amp;#039;&lt;br /&gt;
local PEN_LINK = &amp;#039;([[Penalty kick (association football)|pen.]])&amp;#039;&lt;br /&gt;
local YEL2_CAT = &amp;#039;[[Category:Yellow card template with second parameter]]&amp;#039;&lt;br /&gt;
&lt;br /&gt;
local icon = {&lt;br /&gt;
	goal     = &amp;#039;[[File:Soccerball shade.svg|13px|Goal|link=|alt=]]&amp;#039;,&lt;br /&gt;
	og       = &amp;#039;[[File:Soccerball-red with shade.svg|13px|Own goal|link=|alt=red-colored football]]&amp;#039;,&lt;br /&gt;
	goldgoal = &amp;#039;[[File:Soccerball shade gold.svg|13px|Golden goal|link=|alt=gold-colored football]]&amp;#039;,&lt;br /&gt;
	goldog   = &amp;#039;[[File:Soccerball-red with gold shade.svg|13px|Golden own goal|link=|alt=gold and red-colored football]]&amp;#039;,&lt;br /&gt;
	pengoal  = &amp;#039;[[File:Soccerball shad check.svg|13px|Penalty scored|link=|alt=football with check mark]]&amp;#039;,&lt;br /&gt;
	penmiss  = &amp;#039;[[File:Soccerball shade cross.svg|13px|Penalty missed|link=|alt=football with red X]]&amp;#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- own-goal / penalty shorthand -&amp;gt; canonical modifier (lookup key is lower-cased)&lt;br /&gt;
local modMap = {&lt;br /&gt;
	[&amp;#039;og&amp;#039;] = &amp;#039;og&amp;#039;, [&amp;#039;o.g.&amp;#039;] = &amp;#039;og&amp;#039;, [&amp;#039;o.g&amp;#039;] = &amp;#039;og&amp;#039;,&lt;br /&gt;
	[&amp;#039;pen&amp;#039;] = &amp;#039;pen&amp;#039;, [&amp;#039;pen.&amp;#039;] = &amp;#039;pen&amp;#039;, [&amp;#039;p&amp;#039;] = &amp;#039;pen&amp;#039;, [&amp;#039;p.&amp;#039;] = &amp;#039;pen&amp;#039;,&lt;br /&gt;
	[&amp;#039;pk&amp;#039;] = &amp;#039;pen&amp;#039;, [&amp;#039;p.k.&amp;#039;] = &amp;#039;pen&amp;#039;, [&amp;#039;p.k&amp;#039;] = &amp;#039;pen&amp;#039;,&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- penalty shoot-out keywords -&amp;gt; scored / missed (lookup key is lower-cased)&lt;br /&gt;
local penKW = {}&lt;br /&gt;
for _, w in ipairs({ &amp;#039;goal&amp;#039;, &amp;#039;pengoal&amp;#039;, &amp;#039;score&amp;#039;, &amp;#039;scored&amp;#039; }) do penKW[w] = &amp;#039;goal&amp;#039; end&lt;br /&gt;
for _, w in ipairs({ &amp;#039;miss&amp;#039;, &amp;#039;penmiss&amp;#039;, &amp;#039;missed&amp;#039;, &amp;#039;saved&amp;#039; }) do penKW[w] = &amp;#039;miss&amp;#039; end&lt;br /&gt;
&lt;br /&gt;
-- stage abbreviations for the card templates: key -&amp;gt; {display, title}&lt;br /&gt;
local STAGES = {&lt;br /&gt;
	PRE  = { &amp;#039;PRE&amp;#039;,  &amp;#039;Pre-match&amp;#039; },&lt;br /&gt;
	HT   = { &amp;#039;HT&amp;#039;,   &amp;#039;Half-time&amp;#039; },&lt;br /&gt;
	AWET = { &amp;#039;AWET&amp;#039;, &amp;#039;Awaiting extra time&amp;#039; },&lt;br /&gt;
	HTET = { &amp;#039;HTET&amp;#039;, &amp;#039;Half-time in extra time&amp;#039; },&lt;br /&gt;
	PSO  = { &amp;#039;PSO&amp;#039;,  &amp;#039;Penalty shoot-out&amp;#039; },&lt;br /&gt;
	PK   = { &amp;#039;PSO&amp;#039;,  &amp;#039;Penalty shoot-out&amp;#039; },&lt;br /&gt;
	FT   = { &amp;#039;FT&amp;#039;,   &amp;#039;After full-time&amp;#039; },&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
-- every apostrophe / prime variant&lt;br /&gt;
local APOS = &amp;quot;[`&amp;#039;´ʹʼˈ‘’′]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- small helpers&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local function notBlank(v)&lt;br /&gt;
	return v ~= nil and mw.text.trim(v) ~= &amp;#039;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function inMain()&lt;br /&gt;
	return mw.title.getCurrentTitle().namespace == 0&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function abbr(text, title)&lt;br /&gt;
	return &amp;#039;&amp;lt;abbr title=&amp;quot;&amp;#039; .. title .. &amp;#039;&amp;quot;&amp;gt;&amp;#039; .. text .. &amp;#039;&amp;lt;/abbr&amp;gt;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function normApo(s)&lt;br /&gt;
	return (mw.ustring.gsub(s, APOS, &amp;quot;&amp;#039;&amp;quot;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function delink(s)&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;#039;%[%[[^%[%]|]*|([^%[%]]*)%]%]&amp;#039;, &amp;#039;%1&amp;#039;)&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;#039;%[%[([^%[%]]*)%]%]&amp;#039;, &amp;#039;%1&amp;#039;)&lt;br /&gt;
	return s&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Format a minute and append the minute mark. Stoppage time: an apostrophe&lt;br /&gt;
-- typed before a &amp;quot;+&amp;quot; is dropped and the mark moved to the very end, with any&lt;br /&gt;
-- spaces around the &amp;quot;+&amp;quot; tidied away (&amp;quot;90&amp;#039; + 7&amp;quot; -&amp;gt; &amp;quot;90+7&amp;#039;&amp;quot;).&lt;br /&gt;
local function fmtMin(s)&lt;br /&gt;
	s = normApo(mw.text.trim(s))&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;quot;&amp;#039;%s*(%+)&amp;quot;, &amp;#039;%1&amp;#039;) -- apostrophe before &amp;quot;+&amp;quot;&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;quot;[%s&amp;#039;]*$&amp;quot;, &amp;#039;&amp;#039;)    -- trailing spaces / apostrophes&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;#039;%s*%+%s*&amp;#039;, &amp;#039;+&amp;#039;)  -- tidy spaces around &amp;quot;+&amp;quot;&lt;br /&gt;
	return s .. &amp;quot;&amp;#039;&amp;quot;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- True when, after removing apostrophes / spaces, the value is only digits and&lt;br /&gt;
-- an optional &amp;quot;+&amp;quot; (i.e. a bare minute such as &amp;quot;45&amp;quot;, &amp;quot;90+9&amp;quot;, &amp;quot;90&amp;#039; + 7&amp;quot;). Used to&lt;br /&gt;
-- recover minutes that mis-keyed pipes pushed into an annotation slot.&lt;br /&gt;
local function isMinuteToken(v)&lt;br /&gt;
	local t = mw.ustring.gsub(normApo(v), &amp;quot;[%s&amp;#039;]&amp;quot;, &amp;#039;&amp;#039;)&lt;br /&gt;
	return mw.ustring.match(t, &amp;#039;^%d+%+?%d*$&amp;#039;) ~= nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- card time / stage label.&lt;br /&gt;
--   mode == &amp;#039;plain&amp;#039; -&amp;gt; always a minute&lt;br /&gt;
--   mode == &amp;#039;pso&amp;#039;   -&amp;gt; only PSO/PK/FT recognised (post-reset periods)&lt;br /&gt;
--   default         -&amp;gt; full stage list, otherwise a minute&lt;br /&gt;
local function stageLabel(v, mode)&lt;br /&gt;
	if mode == &amp;#039;plain&amp;#039; then return fmtMin(v) end&lt;br /&gt;
	local up = mw.ustring.upper(mw.text.trim(v))&lt;br /&gt;
	if mode == &amp;#039;pso&amp;#039; then&lt;br /&gt;
		if up == &amp;#039;PSO&amp;#039; or up == &amp;#039;PK&amp;#039; then return abbr(&amp;#039;PSO&amp;#039;, &amp;#039;Penalty shoot-out&amp;#039;) end&lt;br /&gt;
		if up == &amp;#039;FT&amp;#039; then return abbr(&amp;#039;FT&amp;#039;, &amp;#039;After full-time&amp;#039;) end&lt;br /&gt;
		return &amp;#039;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	local s = STAGES[up]&lt;br /&gt;
	if s then return abbr(s[1], s[2]) end&lt;br /&gt;
	return fmtMin(v)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- card icons (variable tooltip)&lt;br /&gt;
local function img(file, px, alt, tt)&lt;br /&gt;
	return &amp;#039;[[File:&amp;#039; .. file .. &amp;#039;|&amp;#039; .. px .. &amp;#039;px|alt=&amp;#039; .. alt .. &amp;#039;|&amp;#039; .. tt .. &amp;#039;|link=]]&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
local function yc(tt)  return img(&amp;#039;Yellow card.svg&amp;#039;,     10, &amp;#039;Yellow card&amp;#039;,     tt) end&lt;br /&gt;
local function rc(tt)  return img(&amp;#039;Red card.svg&amp;#039;,        10, &amp;#039;Red card&amp;#039;,        tt) end&lt;br /&gt;
local function yrc(tt) return img(&amp;#039;Yellow-red card.svg&amp;#039;, 12, &amp;#039;Yellow-red card&amp;#039;, tt) end&lt;br /&gt;
&lt;br /&gt;
local function timed(ic, v, mode)&lt;br /&gt;
	if notBlank(v) then return ic .. &amp;#039;&amp;amp;nbsp;&amp;#039; .. stageLabel(v, mode) end&lt;br /&gt;
	return ic&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- goal rendering (shared by {{goal}}, {{golden goal}} and the box feature)&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local function goalIcon(golden, og)&lt;br /&gt;
	if golden then return og and icon.goldog or icon.goldgoal end&lt;br /&gt;
	return og and icon.og or icon.goal&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- &amp;quot;&amp;lt;span&amp;gt;&amp;lt;minute&amp;gt;[&amp;amp;nbsp;&amp;lt;annotation&amp;gt;]&amp;lt;/span&amp;gt;&amp;quot;&lt;br /&gt;
local function goalSpan(minute, ann)&lt;br /&gt;
	local s, hasMin = &amp;#039;&amp;lt;span&amp;gt;&amp;#039;, notBlank(minute)&lt;br /&gt;
	if hasMin then s = s .. fmtMin(minute) end&lt;br /&gt;
	if ann then&lt;br /&gt;
		if hasMin then s = s .. &amp;#039;&amp;amp;nbsp;&amp;#039; end&lt;br /&gt;
		s = s .. ann&lt;br /&gt;
	end&lt;br /&gt;
	return s .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- raw annotation -&amp;gt; og flag, annotation text&lt;br /&gt;
local function goalAnnot(a)&lt;br /&gt;
	local m = modMap[mw.ustring.lower(mw.text.trim(delink(a)))]&lt;br /&gt;
	if m == &amp;#039;og&amp;#039; then return true, nil end&lt;br /&gt;
	if m == &amp;#039;pen&amp;#039; then return false, PEN_LINK end&lt;br /&gt;
	return false, &amp;#039;(&amp;#039; .. a .. &amp;#039;)&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Try to read a single positional token as a goal entry: either a bare minute&lt;br /&gt;
-- (&amp;quot;33&amp;quot;, &amp;quot;90+9&amp;quot;) or a minute followed by a RECOGNISED modifier, with optional&lt;br /&gt;
-- parentheses (&amp;quot;33 pen&amp;quot;, &amp;quot;90+9 o.g.&amp;quot;, &amp;quot;45 (pen.)&amp;quot;). Returns nil for anything&lt;br /&gt;
-- else (bare modifiers like &amp;quot;pen&amp;quot;, or custom text), so those stay annotations&lt;br /&gt;
-- rather than being mistaken for minutes.&lt;br /&gt;
local function asMinuteEntry(v)&lt;br /&gt;
	if isMinuteToken(v) then&lt;br /&gt;
		return { minute = v, golden = false, og = false, ann = nil }&lt;br /&gt;
	end&lt;br /&gt;
	local minPart, modPart =&lt;br /&gt;
		mw.ustring.match(normApo(mw.text.trim(v)), &amp;#039;^([%d%+%s]-%d)%s*%(?%s*([%a%.]+)%s*%)?$&amp;#039;)&lt;br /&gt;
	if minPart and isMinuteToken(minPart) then&lt;br /&gt;
		local m = modMap[mw.ustring.lower(modPart)]&lt;br /&gt;
		if m then&lt;br /&gt;
			return { minute = minPart, golden = false,&lt;br /&gt;
				og = (m == &amp;#039;og&amp;#039;), ann = (m == &amp;#039;pen&amp;#039;) and PEN_LINK or nil }&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- Render a player&amp;#039;s goal cluster from a list of entries:&lt;br /&gt;
--   entry = { minute = raw, golden = bool, og = bool, ann = text|nil }&lt;br /&gt;
local function renderGoals(entries)&lt;br /&gt;
	local hasOG = false&lt;br /&gt;
	for _, e in ipairs(entries) do&lt;br /&gt;
		if e.og then hasOG = true break end&lt;br /&gt;
	end&lt;br /&gt;
	if hasOG then&lt;br /&gt;
		for _, e in ipairs(entries) do&lt;br /&gt;
			if not e.golden then e.og, e.ann = true, nil end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	local parts, lastG = {}, nil&lt;br /&gt;
	for i, e in ipairs(entries) do&lt;br /&gt;
		local ann = e.ann&lt;br /&gt;
		if e.og then&lt;br /&gt;
			local last = true&lt;br /&gt;
			for j = i + 1, #entries do&lt;br /&gt;
				if entries[j].golden == e.golden and entries[j].og then last = false break end&lt;br /&gt;
			end&lt;br /&gt;
			ann = last and OG_LINK or nil&lt;br /&gt;
		end&lt;br /&gt;
		local span = goalSpan(e.minute, ann)&lt;br /&gt;
		if e.golden ~= lastG then&lt;br /&gt;
			parts[#parts + 1] = goalIcon(e.golden, e.og) .. &amp;#039;&amp;amp;#32;&amp;#039; .. span&lt;br /&gt;
			lastG = e.golden&lt;br /&gt;
		else&lt;br /&gt;
			parts[#parts + 1] = span&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return table.concat(parts, &amp;#039;, &amp;#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- free-text parsing for the box feature&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local function splitLines(s)&lt;br /&gt;
	s = mw.ustring.gsub(s, &amp;#039;&amp;lt;[Bb][Rr]%s*/?%s*&amp;gt;&amp;#039;, &amp;#039;\n&amp;#039;)&lt;br /&gt;
	local out = {}&lt;br /&gt;
	for _, l in ipairs(mw.text.split(s, &amp;#039;\n&amp;#039;)) do&lt;br /&gt;
		l = mw.text.trim((mw.ustring.gsub(l, &amp;#039;^%s*%*%s*&amp;#039;, &amp;#039;&amp;#039;)))&lt;br /&gt;
		if l ~= &amp;#039;&amp;#039; then out[#out + 1] = l end&lt;br /&gt;
	end&lt;br /&gt;
	return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local goldenPrefix = { &amp;#039;golden%s+&amp;#039;, &amp;#039;gold%s+&amp;#039;, &amp;#039;g%.g%.%s*&amp;#039;, &amp;#039;gg%s*&amp;#039; }&lt;br /&gt;
&lt;br /&gt;
-- player name (linked or unlinked) at the start of a goal line&lt;br /&gt;
local function extractPlayer(line)&lt;br /&gt;
	if mw.ustring.sub(line, 1, 2) == &amp;#039;[[&amp;#039; then&lt;br /&gt;
		local e = mw.ustring.find(line, &amp;#039;%]%]&amp;#039;, 3)&lt;br /&gt;
		if e then&lt;br /&gt;
			return mw.ustring.sub(line, 1, e + 1), mw.text.trim(mw.ustring.sub(line, e + 2))&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	-- unlinked: the name ends at the first digit or standalone golden keyword&lt;br /&gt;
	local pos = mw.ustring.find(line, &amp;#039;%d&amp;#039;)&lt;br /&gt;
	local lower = mw.ustring.lower(line)&lt;br /&gt;
	for _, pat in ipairs({ &amp;#039;%s(golden)%s&amp;#039;, &amp;#039;%s(gold)%s&amp;#039;, &amp;#039;%s(gg)%s&amp;#039;, &amp;#039;%s(gg)%d&amp;#039;, &amp;#039;%s(g%.g%.)&amp;#039; }) do&lt;br /&gt;
		local s = mw.ustring.find(lower, pat)&lt;br /&gt;
		if s and (not pos or s + 1 &amp;lt; pos) then pos = s + 1 break end&lt;br /&gt;
	end&lt;br /&gt;
	if pos and pos &amp;gt; 1 then&lt;br /&gt;
		local name = mw.text.trim(mw.ustring.sub(line, 1, pos - 1))&lt;br /&gt;
		if mw.ustring.find(name, &amp;quot;[%-‐‑‒–—―−&amp;#039;]$&amp;quot;) then return line, &amp;#039;&amp;#039; end&lt;br /&gt;
		return name, mw.text.trim(mw.ustring.sub(line, pos))&lt;br /&gt;
	end&lt;br /&gt;
	return line, &amp;#039;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- one comma segment -&amp;gt; goal entry&lt;br /&gt;
local function parseSegment(seg)&lt;br /&gt;
	seg = mw.text.trim(seg)&lt;br /&gt;
	local golden, lower = false, mw.ustring.lower(seg)&lt;br /&gt;
	for _, gp in ipairs(goldenPrefix) do&lt;br /&gt;
		local s, e = mw.ustring.find(lower, &amp;#039;^&amp;#039; .. gp)&lt;br /&gt;
		if s then golden = true; seg = mw.text.trim(mw.ustring.sub(seg, e + 1)) break end&lt;br /&gt;
	end&lt;br /&gt;
	local minute, rest = mw.ustring.match(seg, &amp;quot;^(%d[%d%s%+&amp;#039;]*)(.*)$&amp;quot;)&lt;br /&gt;
	if not minute then return nil end          -- no leading minute -&amp;gt; not a goal&lt;br /&gt;
	local entry = { minute = mw.text.trim(minute), golden = golden, og = false, ann = nil }&lt;br /&gt;
	local mod = mw.ustring.match(rest, &amp;#039;^%s*%(%s*(.-)%s*%)%s*$&amp;#039;) or rest -- &amp;quot;(o.g.)&amp;quot; or &amp;quot;o.g.&amp;quot;&lt;br /&gt;
	mod = mw.text.trim(delink(mod))             -- a linked annotation reduces to its text&lt;br /&gt;
	if mod ~= &amp;#039;&amp;#039; then&lt;br /&gt;
		local m = modMap[mw.ustring.lower(mod)]&lt;br /&gt;
		if m == &amp;#039;og&amp;#039; then entry.og = true&lt;br /&gt;
		elseif m == &amp;#039;pen&amp;#039; then entry.ann = PEN_LINK&lt;br /&gt;
		else entry.unresolved = true end        -- an annotation we do not recognise&lt;br /&gt;
	end&lt;br /&gt;
	return entry&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- a comma-separated run of segments -&amp;gt; list of goal entries&lt;br /&gt;
local function segmentsToEntries(text)&lt;br /&gt;
	local entries = {}&lt;br /&gt;
	for _, seg in ipairs(mw.text.split(text, &amp;#039;,&amp;#039;)) do&lt;br /&gt;
		if mw.text.trim(seg) ~= &amp;#039;&amp;#039; then&lt;br /&gt;
			local e = parseSegment(seg)&lt;br /&gt;
			if not e then return nil end&lt;br /&gt;
			entries[#entries + 1] = e&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return entries&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function joinLines(out, isList)&lt;br /&gt;
	if isList then&lt;br /&gt;
		for i, r in ipairs(out) do out[i] = &amp;#039;*&amp;#039; .. r end&lt;br /&gt;
		return table.concat(out, &amp;#039;\n&amp;#039;)&lt;br /&gt;
	end&lt;br /&gt;
	return table.concat(out, &amp;#039;&amp;lt;br&amp;gt;&amp;#039;)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- returns: out (table of lines), isList, autoUsed&lt;br /&gt;
local function formatGoals(input)&lt;br /&gt;
	input = normApo(input)&lt;br /&gt;
	if mw.ustring.find(input, &amp;#039;[xX×]%d&amp;#039;) then return nil end&lt;br /&gt;
	local isList = mw.ustring.match(mw.text.trim(input), &amp;#039;^%*&amp;#039;) ~= nil&lt;br /&gt;
	local out = {}&lt;br /&gt;
	for _, line in ipairs(splitLines(input)) do&lt;br /&gt;
		local name, rest = extractPlayer(line)&lt;br /&gt;
		if rest == &amp;#039;&amp;#039; then return nil end&lt;br /&gt;
		local entries = segmentsToEntries(rest)&lt;br /&gt;
		if not entries or #entries == 0 then return nil end&lt;br /&gt;
		for _, e in ipairs(entries) do&lt;br /&gt;
			if e.unresolved then return nil end&lt;br /&gt;
		end&lt;br /&gt;
		out[#out + 1] = name .. &amp;#039; &amp;lt;span class=&amp;quot;fb-goal&amp;quot;&amp;gt;&amp;#039; .. renderGoals(entries) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	if #out == 0 then return nil end&lt;br /&gt;
	return out, isList, true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- returns: out (table of lines), isList, autoUsed&lt;br /&gt;
local function formatPenalties(input, isTeam2)&lt;br /&gt;
	local isList = mw.ustring.match(mw.text.trim(input), &amp;#039;^%*&amp;#039;) ~= nil&lt;br /&gt;
	local out, auto = {}, false&lt;br /&gt;
	for _, line in ipairs(splitLines(input)) do&lt;br /&gt;
		local name, kw&lt;br /&gt;
		local ls, le = mw.ustring.find(line, &amp;#039;%[%[.-%]%]&amp;#039;)&lt;br /&gt;
		if ls then&lt;br /&gt;
			name = mw.ustring.sub(line, ls, le)&lt;br /&gt;
			local before = mw.text.trim(mw.ustring.sub(line, 1, ls - 1))&lt;br /&gt;
			local after  = mw.text.trim(mw.ustring.sub(line, le + 1))&lt;br /&gt;
			kw = penKW[mw.ustring.lower(after)] or penKW[mw.ustring.lower(before)]&lt;br /&gt;
		else&lt;br /&gt;
			local lower = mw.ustring.lower(line)&lt;br /&gt;
			for word, res in pairs(penKW) do&lt;br /&gt;
				if mw.ustring.find(lower, &amp;#039;^&amp;#039; .. word .. &amp;#039;%s+&amp;#039;) then&lt;br /&gt;
					kw, name = res, mw.text.trim(mw.ustring.sub(line, #word + 1)) break&lt;br /&gt;
				end&lt;br /&gt;
				local s = mw.ustring.find(lower, &amp;#039;%s+&amp;#039; .. word .. &amp;#039;$&amp;#039;)&lt;br /&gt;
				if s then kw, name = res, mw.text.trim(mw.ustring.sub(line, 1, s - 1)) break end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if name and kw then&lt;br /&gt;
			local ic = kw == &amp;#039;goal&amp;#039; and icon.pengoal or icon.penmiss&lt;br /&gt;
			out[#out + 1] = isTeam2 and (ic .. &amp;#039; &amp;#039; .. name) or (name .. &amp;#039; &amp;#039; .. ic)&lt;br /&gt;
			auto = true&lt;br /&gt;
		else&lt;br /&gt;
			out[#out + 1] = line&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return out, isList, auto&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- argument helpers for the template entry points&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
local function getArgs(frame)&lt;br /&gt;
	local args = {}&lt;br /&gt;
	local parent = frame:getParent()&lt;br /&gt;
	if parent then for k, v in pairs(parent.args) do args[k] = v end end&lt;br /&gt;
	for k, v in pairs(frame.args) do args[k] = v end -- direct #invoke args win&lt;br /&gt;
	return args&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function maxIndex(args)&lt;br /&gt;
	local m = 0&lt;br /&gt;
	for k in pairs(args) do&lt;br /&gt;
		local n = tonumber(k)&lt;br /&gt;
		if n and n == math.floor(n) and n &amp;gt; m then m = n end&lt;br /&gt;
	end&lt;br /&gt;
	return m&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- template entry points&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
-- {{Goal}}&lt;br /&gt;
function p.goal(frame)&lt;br /&gt;
	local args = getArgs(frame)&lt;br /&gt;
	local maxn = maxIndex(args)&lt;br /&gt;
&lt;br /&gt;
	-- gather the non-empty positional values in order&lt;br /&gt;
	local toks, hasComma = {}, false&lt;br /&gt;
	for i = 1, maxn do&lt;br /&gt;
		local v = args[i]&lt;br /&gt;
		if notBlank(v) then&lt;br /&gt;
			toks[#toks + 1] = v&lt;br /&gt;
			if mw.ustring.find(v, &amp;#039;,&amp;#039;) then hasComma = true end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local entries&lt;br /&gt;
	if hasComma then&lt;br /&gt;
		-- free-text dialect (same parser as the box feature)&lt;br /&gt;
		entries = segmentsToEntries(normApo(table.concat(toks, &amp;#039;, &amp;#039;))) or {}&lt;br /&gt;
	else&lt;br /&gt;
		-- pair dialect, tolerant of misplaced pipes&lt;br /&gt;
		entries = {}&lt;br /&gt;
		local current&lt;br /&gt;
		for _, v in ipairs(toks) do&lt;br /&gt;
			local e = asMinuteEntry(v)&lt;br /&gt;
			if e then&lt;br /&gt;
				current = e&lt;br /&gt;
				entries[#entries + 1] = e&lt;br /&gt;
			elseif current and current.ann == nil and not current.og then&lt;br /&gt;
				current.og, current.ann = goalAnnot(v) -- annotate the open minute&lt;br /&gt;
			elseif not current then&lt;br /&gt;
				current = { minute = &amp;#039;&amp;#039;, golden = false, og = false, ann = nil }&lt;br /&gt;
				current.og, current.ann = goalAnnot(v)&lt;br /&gt;
				entries[#entries + 1] = current&lt;br /&gt;
			end&lt;br /&gt;
			-- a stray extra annotation after an already-annotated minute is dropped&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local inner = #entries &amp;gt; 0 and renderGoals(entries) or icon.goal&lt;br /&gt;
	return &amp;#039;&amp;lt;span class=&amp;quot;fb-goal&amp;quot;&amp;gt;&amp;#039; .. inner .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- {{Golden goal}}&lt;br /&gt;
function p.goldengoal(frame)&lt;br /&gt;
	local args = getArgs(frame)&lt;br /&gt;
	local m, a = args[1], args[2]&lt;br /&gt;
	local og, ann = false, nil&lt;br /&gt;
	if notBlank(a) then og, ann = goalAnnot(a) end&lt;br /&gt;
	if notBlank(m) or notBlank(a) then&lt;br /&gt;
		return renderGoals({ { minute = m, golden = true, og = og, ann = ann } })&lt;br /&gt;
	end&lt;br /&gt;
	return goalIcon(true, og)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- {{Yel}} -- yellow card&lt;br /&gt;
function p.yellow(frame)&lt;br /&gt;
	local args = getArgs(frame)&lt;br /&gt;
	local t1, t2 = args[1], args[2]&lt;br /&gt;
	local out = yc(&amp;#039;Booked&amp;#039;)&lt;br /&gt;
	if notBlank(t1) then&lt;br /&gt;
		out = out .. &amp;#039;&amp;lt;span style=&amp;quot;vertical-align: middle;&amp;quot;&amp;gt;&amp;amp;nbsp;&amp;#039; .. stageLabel(t1) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	local up = notBlank(t2) and mw.ustring.upper(mw.text.trim(t2)) or &amp;#039;&amp;#039;&lt;br /&gt;
	if up == &amp;#039;PSO&amp;#039; or up == &amp;#039;PK&amp;#039; or up == &amp;#039;FT&amp;#039; then&lt;br /&gt;
		out = out .. &amp;#039;&amp;amp;nbsp;&amp;#039; .. yc(&amp;#039;Booked immediately before/during/after penalty shoot-out (cards reset)&amp;#039;)&lt;br /&gt;
			.. &amp;#039;&amp;lt;span style=&amp;quot;vertical-align: middle;&amp;quot;&amp;gt;&amp;amp;nbsp;&amp;#039; .. stageLabel(t2, &amp;#039;pso&amp;#039;) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	if inMain() and (notBlank(t2) or notBlank(args[3]) or notBlank(args[4])) then&lt;br /&gt;
		out = out .. YEL2_CAT&lt;br /&gt;
	end&lt;br /&gt;
	return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- {{Sent off}} -- param 1 is the number of bookings before dismissal&lt;br /&gt;
function p.sentoff(frame)&lt;br /&gt;
	local args = getArgs(frame)&lt;br /&gt;
	local mode = mw.text.trim(args[1] or &amp;#039;&amp;#039;)&lt;br /&gt;
	local a2, a3, a4 = args[2], args[3], args[4]&lt;br /&gt;
	local body&lt;br /&gt;
	if mode == &amp;#039;1&amp;#039; then&lt;br /&gt;
		body = timed(yc(&amp;#039;Booked&amp;#039;), a2) .. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(rc(&amp;#039;Sent off (straight red)&amp;#039;), a3)&lt;br /&gt;
	elseif mode == &amp;#039;1+1&amp;#039; then&lt;br /&gt;
		body = timed(yc(&amp;#039;Booked&amp;#039;), a2)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yc(&amp;#039;Booked immediately before/during/after penalty shoot-out (cards reset)&amp;#039;), a3, &amp;#039;pso&amp;#039;)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(rc(&amp;#039;Sent off (straight red) immediately before/during/after penalty shoot-out&amp;#039;), a4, &amp;#039;pso&amp;#039;)&lt;br /&gt;
	elseif mode == &amp;#039;2&amp;#039; then&lt;br /&gt;
		body = timed(yc(&amp;#039;Booked&amp;#039;), a2) .. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yrc(&amp;#039;Sent off (second booking)&amp;#039;), a3)&lt;br /&gt;
	elseif mode == &amp;#039;1+2&amp;#039; then&lt;br /&gt;
		body = timed(yc(&amp;#039;Booked&amp;#039;), a2)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yc(&amp;#039;Booked immediately before/during/after penalty shoot-out (cards reset)&amp;#039;), a3, &amp;#039;pso&amp;#039;)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yrc(&amp;#039;Sent off (second booking) immediately before/during/after penalty shoot-out&amp;#039;), a4, &amp;#039;pso&amp;#039;)&lt;br /&gt;
	elseif mode == &amp;#039;3&amp;#039; then&lt;br /&gt;
		body = timed(yc(&amp;#039;Booked&amp;#039;), a2, &amp;#039;plain&amp;#039;)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yc(&amp;#039;Booked again&amp;#039;), a3, &amp;#039;plain&amp;#039;)&lt;br /&gt;
			.. &amp;#039;&amp;amp;nbsp;&amp;#039; .. timed(yrc(&amp;#039;Sent off (third booking)&amp;#039;), a4, &amp;#039;plain&amp;#039;)&lt;br /&gt;
	else -- 0 / default: straight red&lt;br /&gt;
		body = timed(rc(&amp;#039;Sent off (straight red)&amp;#039;), a2)&lt;br /&gt;
	end&lt;br /&gt;
	return &amp;#039;&amp;lt;span style=&amp;quot;white-space:nowrap;&amp;quot;&amp;gt;&amp;#039; .. body .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- {{Subon}}&lt;br /&gt;
function p.subon(frame)&lt;br /&gt;
	local m = getArgs(frame)[1]&lt;br /&gt;
	local out = &amp;#039;[[File:Sub on.svg|10px|Substituted on|link=|alt=upward-facing green arrow]]&amp;#039;&lt;br /&gt;
	if notBlank(m) then&lt;br /&gt;
		out = out .. &amp;#039;&amp;amp;nbsp;&amp;lt;span style=&amp;quot;vertical-align: middle;&amp;quot;&amp;gt;&amp;#039; .. fmtMin(m) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- {{Suboff}}&lt;br /&gt;
function p.suboff(frame)&lt;br /&gt;
	local args = getArgs(frame)&lt;br /&gt;
	local m = args[1]&lt;br /&gt;
	local out = &amp;#039;[[File:Sub off.svg|10px|Substituted off|link=|alt=downward-facing red arrow]]&amp;#039;&lt;br /&gt;
	if notBlank(m) then&lt;br /&gt;
		out = out .. &amp;#039;&amp;amp;nbsp;&amp;lt;span style=&amp;quot;vertical-align: middle;&amp;quot;&amp;gt;&amp;#039; .. fmtMin(m) .. &amp;#039;&amp;lt;/span&amp;gt;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	if notBlank(args.con) or notBlank(args.concussion) then&lt;br /&gt;
		out = out .. &amp;#039;&amp;amp;nbsp;(&amp;#039; .. abbr(&amp;#039;con.&amp;#039;, &amp;#039;Substituted off due to concussion&amp;#039;) .. &amp;#039;)&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	return out&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
p.yel = p.yellow&lt;br /&gt;
&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
-- module entry point for Module:Football box&lt;br /&gt;
-- --------------------------------------------------------------------------&lt;br /&gt;
&lt;br /&gt;
-- fields: { goals1, goals2, penalties1, penalties2 }&lt;br /&gt;
-- Returns a table of formatted fields plus `auto` = true when any icon was generated.&lt;br /&gt;
function p.formatBox(fields)&lt;br /&gt;
	local res, auto = {}, false&lt;br /&gt;
	local function run(key, fn, ...)&lt;br /&gt;
		local v = fields[key]&lt;br /&gt;
		if v ~= nil and v ~= &amp;#039;&amp;#039; then&lt;br /&gt;
			local out, isList, a = fn(v, ...)&lt;br /&gt;
			if out ~= nil then&lt;br /&gt;
				res[key] = joinLines(out, isList)&lt;br /&gt;
				auto = auto or a&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	run(&amp;#039;goals1&amp;#039;, formatGoals)&lt;br /&gt;
	run(&amp;#039;goals2&amp;#039;, formatGoals)&lt;br /&gt;
	run(&amp;#039;penalties1&amp;#039;, formatPenalties, false)&lt;br /&gt;
	run(&amp;#039;penalties2&amp;#039;, formatPenalties, true)&lt;br /&gt;
	res.auto = auto&lt;br /&gt;
	return res&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Sel Appa</name></author>
	</entry>
</feed>