Module:Football event
-- 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 another module):
-- require('Module:Football event').formatBox{
-- goals1 = ..., goals2 = ..., penalties1 = ..., penalties2 = ... }
local p = {}
-- --------------------------------------------------------------------------
-- shared data
-- --------------------------------------------------------------------------
local OG_LINK = '([[Own goal#Association football|o.g.]])'
local PEN_LINK = '([[Penalty kick (association football)|pen.]])'
local YEL2_CAT = '[[Category:Yellow card template with second parameter]]'
local icon = {
goal = '[[File:Soccerball shade.svg|13px|Goal|link=|alt=]]',
og = '[[File:Soccerball-red with shade.svg|13px|Own goal|link=|alt=red-colored football]]',
goldgoal = '[[File:Soccerball shade gold.svg|13px|Golden goal|link=|alt=gold-colored football]]',
goldog = '[[File:Soccerball-red with gold shade.svg|13px|Golden own goal|link=|alt=gold and red-colored football]]',
pengoal = '[[File:Soccerball shad check.svg|13px|Penalty scored|link=|alt=football with check mark]]',
penmiss = '[[File:Soccerball shade cross.svg|13px|Penalty missed|link=|alt=football with red X]]',
}
-- own-goal / penalty shorthand -> canonical modifier (lookup key is lower-cased)
local modMap = {
['og'] = 'og', ['o.g.'] = 'og', ['o.g'] = 'og',
['pen'] = 'pen', ['pen.'] = 'pen', ['p'] = 'pen', ['p.'] = 'pen',
['pk'] = 'pen', ['p.k.'] = 'pen', ['p.k'] = 'pen',
}
-- penalty shoot-out keywords -> scored / missed (lookup key is lower-cased)
local penKW = {}
for _, w in ipairs({ 'goal', 'pengoal', 'score', 'scored' }) do penKW[w] = 'goal' end
for _, w in ipairs({ 'miss', 'penmiss', 'missed', 'saved' }) do penKW[w] = 'miss' end
-- stage abbreviations for the card templates: key -> {display, title}
local STAGES = {
PRE = { 'PRE', 'Pre-match' },
HT = { 'HT', 'Half-time' },
AWET = { 'AWET', 'Awaiting extra time' },
HTET = { 'HTET', 'Half-time in extra time' },
PSO = { 'PSO', 'Penalty shoot-out' },
PK = { 'PSO', 'Penalty shoot-out' },
FT = { 'FT', 'After full-time' },
}
-- every apostrophe / prime variant
local APOS = "[`'´ʹʼˈ‘’′]"
-- --------------------------------------------------------------------------
-- small helpers
-- --------------------------------------------------------------------------
local function notBlank(v)
return v ~= nil and mw.text.trim(v) ~= ''
end
local function inMain()
return mw.title.getCurrentTitle().namespace == 0
end
local function abbr(text, title)
return '<abbr title="' .. title .. '">' .. text .. '</abbr>'
end
local function normApo(s)
return (mw.ustring.gsub(s, APOS, "'"))
end
local function delink(s)
s = mw.ustring.gsub(s, '%[%[[^%[%]|]*|([^%[%]]*)%]%]', '%1')
s = mw.ustring.gsub(s, '%[%[([^%[%]]*)%]%]', '%1')
return s
end
-- Format a minute and append the minute mark. Stoppage time: an apostrophe
-- typed before a "+" is dropped and the mark moved to the very end, with any
-- spaces around the "+" tidied away ("90' + 7" -> "90+7'").
local function fmtMin(s)
s = normApo(mw.text.trim(s))
s = mw.ustring.gsub(s, "'%s*(%+)", '%1') -- apostrophe before "+"
s = mw.ustring.gsub(s, "[%s']*$", '') -- trailing spaces / apostrophes
s = mw.ustring.gsub(s, '%s*%+%s*', '+') -- tidy spaces around "+"
return s .. "'"
end
-- True when, after removing apostrophes / spaces, the value is only digits and
-- an optional "+" (i.e. a bare minute such as "45", "90+9", "90' + 7"). Used to
-- recover minutes that mis-keyed pipes pushed into an annotation slot.
local function isMinuteToken(v)
local t = mw.ustring.gsub(normApo(v), "[%s']", '')
return mw.ustring.match(t, '^%d+%+?%d*$') ~= nil
end
-- card time / stage label.
-- mode == 'plain' -> always a minute
-- mode == 'pso' -> only PSO/PK/FT recognised (post-reset periods)
-- default -> full stage list, otherwise a minute
local function stageLabel(v, mode)
if mode == 'plain' then return fmtMin(v) end
local up = mw.ustring.upper(mw.text.trim(v))
if mode == 'pso' then
if up == 'PSO' or up == 'PK' then return abbr('PSO', 'Penalty shoot-out') end
if up == 'FT' then return abbr('FT', 'After full-time') end
return ''
end
local s = STAGES[up]
if s then return abbr(s[1], s[2]) end
return fmtMin(v)
end
-- card icons (variable tooltip)
local function img(file, px, alt, tt)
return '[[File:' .. file .. '|' .. px .. 'px|alt=' .. alt .. '|' .. tt .. '|link=]]'
end
local function yc(tt) return img('Yellow card.svg', 10, 'Yellow card', tt) end
local function rc(tt) return img('Red card.svg', 10, 'Red card', tt) end
local function yrc(tt) return img('Yellow-red card.svg', 12, 'Yellow-red card', tt) end
local function timed(ic, v, mode)
if notBlank(v) then return ic .. ' ' .. stageLabel(v, mode) end
return ic
end
-- --------------------------------------------------------------------------
-- goal rendering (shared by {{goal}}, {{golden goal}} and the box feature)
-- --------------------------------------------------------------------------
local function goalIcon(golden, og)
if golden then return og and icon.goldog or icon.goldgoal end
return og and icon.og or icon.goal
end
-- "<span><minute>[ <annotation>]</span>"
local function goalSpan(minute, ann)
local s, hasMin = '<span>', notBlank(minute)
if hasMin then s = s .. fmtMin(minute) end
if ann then
if hasMin then s = s .. ' ' end
s = s .. ann
end
return s .. '</span>'
end
-- raw annotation -> og flag, annotation text
local function goalAnnot(a)
local m = modMap[mw.ustring.lower(mw.text.trim(delink(a)))]
if m == 'og' then return true, nil end
if m == 'pen' then return false, PEN_LINK end
return false, '(' .. a .. ')'
end
-- Try to read a single positional token as a goal entry: either a bare minute
-- ("33", "90+9") or a minute followed by a RECOGNISED modifier, with optional
-- parentheses ("33 pen", "90+9 o.g.", "45 (pen.)"). Returns nil for anything
-- else (bare modifiers like "pen", or custom text), so those stay annotations
-- rather than being mistaken for minutes.
local function asMinuteEntry(v)
if isMinuteToken(v) then
return { minute = v, golden = false, og = false, ann = nil }
end
local minPart, modPart =
mw.ustring.match(normApo(mw.text.trim(v)), '^([%d%+%s]-%d)%s*%(?%s*([%a%.]+)%s*%)?$')
if minPart and isMinuteToken(minPart) then
local m = modMap[mw.ustring.lower(modPart)]
if m then
return { minute = minPart, golden = false,
og = (m == 'og'), ann = (m == 'pen') and PEN_LINK or nil }
end
end
return nil
end
-- Render a player's goal cluster from a list of entries:
-- entry = { minute = raw, golden = bool, og = bool, ann = text|nil }
local function renderGoals(entries)
local hasOG = false
for _, e in ipairs(entries) do
if e.og then hasOG = true break end
end
if hasOG then
for _, e in ipairs(entries) do
if not e.golden then e.og, e.ann = true, nil end
end
end
local parts, lastG = {}, nil
for i, e in ipairs(entries) do
local ann = e.ann
if e.og then
local last = true
for j = i + 1, #entries do
if entries[j].golden == e.golden and entries[j].og then last = false break end
end
ann = last and OG_LINK or nil
end
local span = goalSpan(e.minute, ann)
if e.golden ~= lastG then
parts[#parts + 1] = goalIcon(e.golden, e.og) .. ' ' .. span
lastG = e.golden
else
parts[#parts + 1] = span
end
end
return table.concat(parts, ', ')
end
-- --------------------------------------------------------------------------
-- free-text parsing for the box feature
-- --------------------------------------------------------------------------
local function splitLines(s)
s = mw.ustring.gsub(s, '<[Bb][Rr]%s*/?%s*>', '\n')
local out = {}
for _, l in ipairs(mw.text.split(s, '\n')) do
l = mw.text.trim((mw.ustring.gsub(l, '^%s*%*%s*', '')))
if l ~= '' then out[#out + 1] = l end
end
return out
end
local goldenPrefix = { 'golden%s+', 'gold%s+', 'g%.g%.%s*', 'gg%s*' }
-- player name (linked or unlinked) at the start of a goal line
local function extractPlayer(line)
if mw.ustring.sub(line, 1, 2) == '[[' then
local e = mw.ustring.find(line, '%]%]', 3)
if e then
return mw.ustring.sub(line, 1, e + 1), mw.text.trim(mw.ustring.sub(line, e + 2))
end
end
-- unlinked: the name ends at the first digit or standalone golden keyword
local pos = mw.ustring.find(line, '%d')
local lower = mw.ustring.lower(line)
for _, pat in ipairs({ '%s(golden)%s', '%s(gold)%s', '%s(gg)%s', '%s(gg)%d', '%s(g%.g%.)' }) do
local s = mw.ustring.find(lower, pat)
if s and (not pos or s + 1 < pos) then pos = s + 1 break end
end
if pos and pos > 1 then
return mw.text.trim(mw.ustring.sub(line, 1, pos - 1)), mw.text.trim(mw.ustring.sub(line, pos))
end
return line, ''
end
-- one comma segment -> goal entry
local function parseSegment(seg)
seg = mw.text.trim(seg)
local golden, lower = false, mw.ustring.lower(seg)
for _, gp in ipairs(goldenPrefix) do
local s, e = mw.ustring.find(lower, '^' .. gp)
if s then golden = true; seg = mw.text.trim(mw.ustring.sub(seg, e + 1)) break end
end
local minute, rest = mw.ustring.match(seg, "^(%d[%d%s%+']*)(.*)$")
if not minute then return nil end -- no leading minute -> not a goal
local entry = { minute = mw.text.trim(minute), golden = golden, og = false, ann = nil }
local mod = mw.ustring.match(rest, '^%s*%(%s*(.-)%s*%)%s*$') or rest -- "(o.g.)" or "o.g."
mod = mw.text.trim(delink(mod)) -- a linked annotation reduces to its text
if mod ~= '' then
local m = modMap[mw.ustring.lower(mod)]
if m == 'og' then entry.og = true
elseif m == 'pen' then entry.ann = PEN_LINK
else entry.unresolved = true end -- an annotation we do not recognise
end
return entry
end
-- a comma-separated run of segments -> list of goal entries
local function segmentsToEntries(text)
local entries = {}
for _, seg in ipairs(mw.text.split(text, ',')) do
if mw.text.trim(seg) ~= '' then
local e = parseSegment(seg)
if not e then return nil end
entries[#entries + 1] = e
end
end
return entries
end
local function joinLines(out, isList)
if isList then
for i, r in ipairs(out) do out[i] = '*' .. r end
return table.concat(out, '\n')
end
return table.concat(out, '<br>')
end
-- returns: out (table of lines), isList, autoUsed
local function formatGoals(input)
input = normApo(input)
if mw.ustring.find(input, '[xX×]%d') then return nil end
local isList = mw.ustring.match(mw.text.trim(input), '^%*') ~= nil
local out = {}
for _, line in ipairs(splitLines(input)) do
local name, rest = extractPlayer(line)
if rest == '' then return nil end
local entries = segmentsToEntries(rest)
if not entries or #entries == 0 then return nil end
for _, e in ipairs(entries) do
if e.unresolved then return nil end
end
out[#out + 1] = name .. ' <span class="fb-goal">' .. renderGoals(entries) .. '</span>'
end
if #out == 0 then return nil end
return out, isList, true
end
-- returns: out (table of lines), isList, autoUsed
local function formatPenalties(input, isTeam2)
local isList = mw.ustring.match(mw.text.trim(input), '^%*') ~= nil
local out, auto = {}, false
for _, line in ipairs(splitLines(input)) do
local name, kw
local ls, le = mw.ustring.find(line, '%[%[.-%]%]')
if ls then
name = mw.ustring.sub(line, ls, le)
local before = mw.text.trim(mw.ustring.sub(line, 1, ls - 1))
local after = mw.text.trim(mw.ustring.sub(line, le + 1))
kw = penKW[mw.ustring.lower(after)] or penKW[mw.ustring.lower(before)]
else
local lower = mw.ustring.lower(line)
for word, res in pairs(penKW) do
if mw.ustring.find(lower, '^' .. word .. '%s+') then
kw, name = res, mw.text.trim(mw.ustring.sub(line, #word + 1)) break
end
local s = mw.ustring.find(lower, '%s+' .. word .. '$')
if s then kw, name = res, mw.text.trim(mw.ustring.sub(line, 1, s - 1)) break end
end
end
if name and kw then
local ic = kw == 'goal' and icon.pengoal or icon.penmiss
out[#out + 1] = isTeam2 and (ic .. ' ' .. name) or (name .. ' ' .. ic)
auto = true
else
out[#out + 1] = line
end
end
return out, isList, auto
end
-- --------------------------------------------------------------------------
-- argument helpers for the template entry points
-- --------------------------------------------------------------------------
local function getArgs(frame)
local args = {}
local parent = frame:getParent()
if parent then for k, v in pairs(parent.args) do args[k] = v end end
for k, v in pairs(frame.args) do args[k] = v end -- direct #invoke args win
return args
end
local function maxIndex(args)
local m = 0
for k in pairs(args) do
local n = tonumber(k)
if n and n == math.floor(n) and n > m then m = n end
end
return m
end
-- --------------------------------------------------------------------------
-- template entry points
-- --------------------------------------------------------------------------
-- {{Goal}}
function p.goal(frame)
local args = getArgs(frame)
local maxn = maxIndex(args)
-- gather the non-empty positional values in order
local toks, hasComma = {}, false
for i = 1, maxn do
local v = args[i]
if notBlank(v) then
toks[#toks + 1] = v
if mw.ustring.find(v, ',') then hasComma = true end
end
end
local entries
if hasComma then
-- free-text dialect (same parser as the box feature)
entries = segmentsToEntries(normApo(table.concat(toks, ', '))) or {}
else
-- pair dialect, tolerant of misplaced pipes
entries = {}
local current
for _, v in ipairs(toks) do
local e = asMinuteEntry(v)
if e then
current = e
entries[#entries + 1] = e
elseif current and current.ann == nil and not current.og then
current.og, current.ann = goalAnnot(v) -- annotate the open minute
elseif not current then
current = { minute = '', golden = false, og = false, ann = nil }
current.og, current.ann = goalAnnot(v)
entries[#entries + 1] = current
end
-- a stray extra annotation after an already-annotated minute is dropped
end
end
local inner = #entries > 0 and renderGoals(entries) or icon.goal
return '<span class="fb-goal">' .. inner .. '</span>'
end
-- {{Golden goal}}
function p.goldengoal(frame)
local args = getArgs(frame)
local m, a = args[1], args[2]
local og, ann = false, nil
if notBlank(a) then og, ann = goalAnnot(a) end
if notBlank(m) or notBlank(a) then
return renderGoals({ { minute = m, golden = true, og = og, ann = ann } })
end
return goalIcon(true, og)
end
-- {{Yel}} -- yellow card
function p.yellow(frame)
local args = getArgs(frame)
local t1, t2 = args[1], args[2]
local out = yc('Booked')
if notBlank(t1) then
out = out .. '<span style="vertical-align: middle;"> ' .. stageLabel(t1) .. '</span>'
end
local up = notBlank(t2) and mw.ustring.upper(mw.text.trim(t2)) or ''
if up == 'PSO' or up == 'PK' or up == 'FT' then
out = out .. ' ' .. yc('Booked immediately before/during/after penalty shoot-out (cards reset)')
.. '<span style="vertical-align: middle;"> ' .. stageLabel(t2, 'pso') .. '</span>'
end
if inMain() and (notBlank(t2) or notBlank(args[3]) or notBlank(args[4])) then
out = out .. YEL2_CAT
end
return out
end
-- {{Sent off}} -- param 1 is the number of bookings before dismissal
function p.sentoff(frame)
local args = getArgs(frame)
local mode = mw.text.trim(args[1] or '')
local a2, a3, a4 = args[2], args[3], args[4]
local body
if mode == '1' then
body = timed(yc('Booked'), a2) .. ' ' .. timed(rc('Sent off (straight red)'), a3)
elseif mode == '1+1' then
body = timed(yc('Booked'), a2)
.. ' ' .. timed(yc('Booked immediately before/during/after penalty shoot-out (cards reset)'), a3, 'pso')
.. ' ' .. timed(rc('Sent off (straight red) immediately before/during/after penalty shoot-out'), a4, 'pso')
elseif mode == '2' then
body = timed(yc('Booked'), a2) .. ' ' .. timed(yrc('Sent off (second booking)'), a3)
elseif mode == '1+2' then
body = timed(yc('Booked'), a2)
.. ' ' .. timed(yc('Booked immediately before/during/after penalty shoot-out (cards reset)'), a3, 'pso')
.. ' ' .. timed(yrc('Sent off (second booking) immediately before/during/after penalty shoot-out'), a4, 'pso')
elseif mode == '3' then
body = timed(yc('Booked'), a2, 'plain')
.. ' ' .. timed(yc('Booked again'), a3, 'plain')
.. ' ' .. timed(yrc('Sent off (third booking)'), a4, 'plain')
else -- 0 / default: straight red
body = timed(rc('Sent off (straight red)'), a2)
end
return '<span style="white-space:nowrap;">' .. body .. '</span>'
end
-- {{Subon}}
function p.subon(frame)
local m = getArgs(frame)[1]
local out = '[[File:Sub on.svg|10px|Substituted on|link=|alt=upward-facing green arrow]]'
if notBlank(m) then
out = out .. ' <span style="vertical-align: middle;">' .. fmtMin(m) .. '</span>'
end
return out
end
-- {{Suboff}}
function p.suboff(frame)
local args = getArgs(frame)
local m = args[1]
local out = '[[File:Sub off.svg|10px|Substituted off|link=|alt=downward-facing red arrow]]'
if notBlank(m) then
out = out .. ' <span style="vertical-align: middle;">' .. fmtMin(m) .. '</span>'
end
if notBlank(args.con) or notBlank(args.concussion) then
out = out .. ' (' .. abbr('con.', 'Substituted off due to concussion') .. ')'
end
return out
end
p.yel = p.yellow
-- --------------------------------------------------------------------------
-- module entry point for Module:Football box
-- --------------------------------------------------------------------------
-- fields: { goals1, goals2, penalties1, penalties2 }
-- Returns a table of formatted fields plus `auto` = true when any icon was generated.
function p.formatBox(fields)
local res, auto = {}, false
local function run(key, fn, ...)
local v = fields[key]
if v ~= nil and v ~= '' then
local out, isList, a = fn(v, ...)
if out ~= nil then
res[key] = joinLines(out, isList)
auto = auto or a
end
end
end
run('goals1', formatGoals)
run('goals2', formatGoals)
run('penalties1', formatPenalties, false)
run('penalties2', formatPenalties, true)
res.auto = auto
return res
end
return p
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.