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 .. '&nbsp;' .. 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>[&nbsp;<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 .. '&nbsp;' 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) .. '&#32;' .. 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;">&nbsp;' .. 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 .. '&nbsp;' .. yc('Booked immediately before/during/after penalty shoot-out (cards reset)')
			.. '<span style="vertical-align: middle;">&nbsp;' .. 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) .. '&nbsp;' .. timed(rc('Sent off (straight red)'), a3)
	elseif mode == '1+1' then
		body = timed(yc('Booked'), a2)
			.. '&nbsp;' .. timed(yc('Booked immediately before/during/after penalty shoot-out (cards reset)'), a3, 'pso')
			.. '&nbsp;' .. timed(rc('Sent off (straight red) immediately before/during/after penalty shoot-out'), a4, 'pso')
	elseif mode == '2' then
		body = timed(yc('Booked'), a2) .. '&nbsp;' .. timed(yrc('Sent off (second booking)'), a3)
	elseif mode == '1+2' then
		body = timed(yc('Booked'), a2)
			.. '&nbsp;' .. timed(yc('Booked immediately before/during/after penalty shoot-out (cards reset)'), a3, 'pso')
			.. '&nbsp;' .. 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')
			.. '&nbsp;' .. timed(yc('Booked again'), a3, 'plain')
			.. '&nbsp;' .. 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 .. '&nbsp;<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 .. '&nbsp;<span style="vertical-align: middle;">' .. fmtMin(m) .. '</span>'
	end
	if notBlank(args.con) or notBlank(args.concussion) then
		out = out .. '&nbsp;(' .. 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.

  1. 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:
  2. 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.
  3. 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.
  4. 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.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.