Module:Football group summary
Appearance
local p = {}
local MONTHS = {
january = 1, february = 2, march = 3, april = 4,
may = 5, june = 6, july = 7, august = 8,
september = 9, october = 10, november = 11, december = 12,
}
local MONTH_NAMES = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
}
local function trim(s)
if not s then return "" end
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function normalizeSpaces(s)
if not s then return "" end
s = s:gsub(" ", " ")
s = s:gsub("\194\160", " ")
s = s:gsub("&", "&")
return s
end
local function findTemplateEnd(s, openStart)
local i = openStart + 2
local depth = 1
local len = #s
while i <= len do
local c2 = s:sub(i, i + 1)
if c2 == "{{" then
depth = depth + 1
i = i + 2
elseif c2 == "}}" then
depth = depth - 1
if depth == 0 then
return i + 1
end
i = i + 2
else
i = i + 1
end
end
return nil
end
local function splitTopLevelPipes(body)
local parts = {}
local buf = {}
local i = 1
local len = #body
local braceDepth = 0
local bracketDepth = 0
local tagDepth = 0
while i <= len do
local c = body:sub(i, i)
local c2 = body:sub(i, i + 1)
if c2 == "{{" then
braceDepth = braceDepth + 1
buf[#buf + 1] = c2
i = i + 2
elseif c2 == "}}" then
braceDepth = math.max(0, braceDepth - 1)
buf[#buf + 1] = c2
i = i + 2
elseif c2 == "[[" then
bracketDepth = bracketDepth + 1
buf[#buf + 1] = c2
i = i + 2
elseif c2 == "]]" then
bracketDepth = math.max(0, bracketDepth - 1)
buf[#buf + 1] = c2
i = i + 2
elseif c == "<" then
tagDepth = tagDepth + 1
buf[#buf + 1] = c
i = i + 1
elseif c == ">" then
tagDepth = math.max(0, tagDepth - 1)
buf[#buf + 1] = c
i = i + 1
elseif c == "|" and braceDepth == 0 and bracketDepth == 0 and tagDepth == 0 then
parts[#parts + 1] = table.concat(buf)
buf = {}
i = i + 1
else
buf[#buf + 1] = c
i = i + 1
end
end
parts[#parts + 1] = table.concat(buf)
return parts
end
local function parseTemplateBody(body)
local segments = splitTopLevelPipes(body)
local result = { _name = trim(segments[1] or "") }
local positional = 0
for i = 2, #segments do
local seg = segments[i]
local eqPos
local depth, brack = 0, 0
for j = 1, #seg do
local c = seg:sub(j, j)
local c2 = seg:sub(j, j + 1)
if c2 == "{{" then depth = depth + 1
elseif c2 == "}}" then depth = depth - 1
elseif c2 == "[[" then brack = brack + 1
elseif c2 == "]]" then brack = brack - 1
elseif c == "=" and depth == 0 and brack == 0 then
eqPos = j
break
end
end
if eqPos then
local name = trim(seg:sub(1, eqPos - 1))
local value = trim(seg:sub(eqPos + 1))
result[name] = value
else
positional = positional + 1
result[positional] = trim(seg)
end
end
return result
end
local function stripQuotes(s)
if not s then return s end
s = trim(s)
local q = s:sub(1, 1)
if (q == '"' or q == "'") and s:sub(-1) == q then
return s:sub(2, -2)
end
return s
end
local function extractLabeledSection(wikitext, sectionName)
local target = trim(sectionName)
local out = {}
local i = 1
local len = #wikitext
while i <= len do
local tagStart, tagEnd, attrs =
wikitext:find("<%s*[Ss][Ee][Cc][Tt][Ii][Oo][Nn]%s+([^<>]-)/?%s*>", i)
if not tagStart then break end
local kind = attrs:match("^%s*([Bb][Ee][Gg][Ii][Nn])%s*=") and "begin"
or attrs:match("^%s*([Ee][Nn][Dd])%s*=") and "end"
local nameMatch
if kind then
nameMatch = attrs:match("=%s*(.+)%s*$")
if nameMatch then nameMatch = stripQuotes(nameMatch) end
end
if kind == "begin" and nameMatch == target then
local searchFrom = tagEnd + 1
while true do
local eStart, eEnd, eAttrs =
wikitext:find("<%s*[Ss][Ee][Cc][Tt][Ii][Oo][Nn]%s+([^<>]-)/?%s*>", searchFrom)
if not eStart then break end
local eKind = eAttrs:match("^%s*([Ee][Nn][Dd])%s*=") and "end"
or eAttrs:match("^%s*([Bb][Ee][Gg][Ii][Nn])%s*=") and "begin"
local eName = eAttrs:match("=%s*(.+)%s*$")
if eName then eName = stripQuotes(eName) end
if eKind == "end" and eName == target then
out[#out + 1] = wikitext:sub(tagEnd + 1, eStart - 1)
i = eEnd + 1
break
end
searchFrom = eEnd + 1
end
if i <= tagEnd then i = tagEnd + 1 end
else
i = tagEnd + 1
end
end
return table.concat(out, "\n")
end
local function extractHeaderSection(wikitext, headerName)
local target = trim(headerName)
local lines = {}
local lineStart = 1
for s in wikitext:gmatch("[^\n]*\n?") do
if #s == 0 then break end
lines[#lines + 1] = { text = s, start = lineStart }
lineStart = lineStart + #s
end
local found
local foundLevel
for idx, line in ipairs(lines) do
local eqL, name, eqR = line.text:match("^%s*(=+)%s*(.-)%s*(=+)%s*$")
if eqL and name and eqR then
local level = math.min(#eqL, #eqR)
if trim(name) == target then
found = idx
foundLevel = level
break
end
end
end
if not found then return "" end
local startIdx = found + 1
local endIdx = #lines
for idx = startIdx, #lines do
local line = lines[idx]
local eqL, _, eqR = line.text:match("^%s*(=+)%s*(.-)%s*(=+)%s*$")
if eqL and eqR then
local level = math.min(#eqL, #eqR)
if level <= foundLevel then
endIdx = idx - 1
break
end
end
end
local out = {}
for idx = startIdx, endIdx do
out[#out + 1] = lines[idx].text
end
return table.concat(out)
end
local function fetchArticle(titleStr)
if not titleStr or trim(titleStr) == "" then return "" end
local title = mw.title.new(trim(titleStr))
if not title or not title.exists then return "" end
return title:getContent() or ""
end
local function identifyTransclusion(wikitext, openStart)
local closeEnd = findTemplateEnd(wikitext, openStart)
if not closeEnd then return nil end
local body = wikitext:sub(openStart + 2, closeEnd - 2)
local segments = splitTopLevelPipes(body)
local head = trim(segments[1] or "")
local fullTitle = head:match("^:%s*(.+)$")
if fullTitle then
return "full", trim(fullTitle), nil, closeEnd
end
local fn, target = head:match("^#%s*([%w%-]+)%s*:%s*(.+)$")
if fn and target then
local fnLower = fn:lower()
local arg = trim(segments[2] or "")
if fnLower == "lst" or fnLower == "section" then
return "lst", trim(target), arg, closeEnd
elseif fnLower == "lsth" or fnLower == "section-h" then
return "lsth", trim(target), arg, closeEnd
end
end
return nil
end
local function findFootballBoxInvocations(wikitext, fetchFn)
fetchFn = fetchFn or fetchArticle
local results = {}
local i = 1
local len = #wikitext
while i <= len do
local openStart = wikitext:find("{{", i, true)
if not openStart then break end
local after = wikitext:sub(openStart + 2, openStart + 80):lower()
local stripped = after:gsub("^%s+", "")
local isFootballBox = false
if stripped:match("^football%s*box[%s|}]") then
isFootballBox = true
elseif stripped:match("^#invoke:%s*football%s*box%s*|%s*main[%s|}]") then
isFootballBox = true
end
if isFootballBox then
local closeEnd = findTemplateEnd(wikitext, openStart)
if closeEnd then
local body = wikitext:sub(openStart + 2, closeEnd - 2)
results[#results + 1] = body
i = closeEnd + 1
else
i = openStart + 2
end
else
local kind, target, arg, closeEnd =
identifyTransclusion(wikitext, openStart)
if kind and target then
local fetched = fetchFn(target)
local slice = ""
if fetched and fetched ~= "" then
if kind == "full" then
slice = fetched
elseif kind == "lst" then
slice = extractLabeledSection(fetched, arg or "")
elseif kind == "lsth" then
slice = extractHeaderSection(fetched, arg or "")
end
end
if slice and slice ~= "" then
local nestedBoxes = findFootballBoxInvocations(slice,
function() return "" end)
for _, b in ipairs(nestedBoxes) do
results[#results + 1] = b
end
end
i = (closeEnd or (openStart + 1)) + 1
else
i = openStart + 2
end
end
end
return results
end
local function findNestedTemplate(s, targetName)
local normalizedTarget = targetName:lower():gsub("%s+", " ")
local i = 1
local len = #s
while i <= len do
local openStart = s:find("{{", i, true)
if not openStart then return nil end
local closeEnd = findTemplateEnd(s, openStart)
if not closeEnd then return nil end
local body = s:sub(openStart + 2, closeEnd - 2)
local firstPipe = body:find("|", 1, true)
local name
if firstPipe then
name = trim(body:sub(1, firstPipe - 1))
else
name = trim(body)
end
local normalizedName = name:lower():gsub("%s+", " ")
if normalizedName == normalizedTarget then
return openStart, closeEnd, parseTemplateBody(body)
end
i = openStart + 2
end
return nil
end
local function formatDate(d, style)
if not d then return "" end
if style == "mdy" then
return string.format("%s %d, %d", MONTH_NAMES[d.month], d.day, d.year)
else
return string.format("%d %s %d", d.day, MONTH_NAMES[d.month], d.year)
end
end
local function parseDateParam(value)
if not value or value == "" then return nil, "" end
value = trim(value)
local sdOpen = value:match("^{{%s*[Ss][Tt][Aa][Rr][Tt]%s+[Dd][Aa][Tt][Ee]%s*[|}]")
if sdOpen then
local closeEnd = findTemplateEnd(value, 1)
if closeEnd then
local body = value:sub(3, closeEnd - 2)
local params = parseTemplateBody(body)
local y = tonumber(params[1])
local m = tonumber(params[2])
local d = tonumber(params[3])
if y and m and d and m >= 1 and m <= 12 then
local style
if params["df"] and trim(params["df"]):lower() == "y" then
style = "dmy"
elseif params["mf"] and trim(params["mf"]):lower() == "y" then
style = "mdy"
else
style = "mdy"
end
local dt = { year = y, month = m, day = d }
return dt, formatDate(dt, style)
end
end
end
local plain = normalizeSpaces(value)
plain = plain:gsub("%[%[[^%[%]|]-|([^%[%]]-)%]%]", "%1")
plain = plain:gsub("%[%[([^%[%]]-)%]%]", "%1")
plain = plain:gsub("<[^<>]->", "")
plain = trim(plain:gsub("%s+", " "))
local y, m, d = plain:match("(%d%d%d%d)%-(%d%d)%-(%d%d)")
if y then
local dt = { year = tonumber(y), month = tonumber(m), day = tonumber(d) }
return dt, formatDate(dt, "mdy")
end
local day, mname, year = plain:match("(%d%d?)%s+([A-Za-z]+)%s+(%d%d%d%d)")
if day and mname and year then
local mi = MONTHS[mname:lower()]
if mi then
local dt = { year = tonumber(year), month = mi, day = tonumber(day) }
return dt, plain
end
end
local mname2, day2, year2 = plain:match("([A-Za-z]+)%s+(%d%d?),?%s+(%d%d%d%d)")
if mname2 and day2 and year2 then
local mi = MONTHS[mname2:lower()]
if mi then
local dt = { year = tonumber(year2), month = mi, day = tonumber(day2) }
return dt, plain
end
end
return nil, plain
end
local function buildScoreOutput(scoreValue, aetValue)
scoreValue = scoreValue or ""
local output
local startPos, endPos, params = findNestedTemplate(scoreValue, "score link")
if startPos and params then
local p1 = params[1] or ""
local p2 = params[2] or ""
local linked = "[[" .. p1 .. "|" .. p2 .. "]]"
local before = scoreValue:sub(1, startPos - 1)
local after = scoreValue:sub(endPos + 1)
output = before .. linked .. after
else
output = scoreValue
end
output = trim(output)
if aetValue and trim(aetValue) ~= "" then
output = output .. " ([[Overtime (sports)#Association football|a.e.t.]])"
end
return output
end
local function buildMatchRecord(params, sourceOrder)
local record = { sourceOrder = sourceOrder }
local dt, dateStr = parseDateParam(params["date"])
record.date = dt
record.dateStr = dateStr
record.home = trim(params["team1"] or "")
record.away = trim(params["team2"] or "")
record.score = buildScoreOutput(params["score"] or "", params["aet"])
local stadium = trim(params["stadium"] or "")
local location = trim(params["location"] or "")
if stadium ~= "" and location ~= "" then
record.venue = stadium .. ", " .. location
elseif stadium ~= "" then
record.venue = stadium
else
record.venue = location
end
return record
end
local function sortMatches(matches)
table.sort(matches, function(a, b)
local ad, bd = a.date, b.date
if ad and bd then
if ad.year ~= bd.year then return ad.year < bd.year end
if ad.month ~= bd.month then return ad.month < bd.month end
if ad.day ~= bd.day then return ad.day < bd.day end
return a.sourceOrder < b.sourceOrder
elseif ad and not bd then
return true
elseif not ad and bd then
return false
else
return a.sourceOrder < b.sourceOrder
end
end)
end
local function buildTable(matches)
local out = {}
out[#out + 1] = "{{table alignment}}"
out[#out + 1] = '{| class="col1right col2center" style="width:100%" cellspacing="1"'
out[#out + 1] = "|-"
out[#out + 1] = '!style="width:25%"|'
out[#out + 1] = '!style="width:10%"|'
out[#out + 1] = '!style="width:25%"|'
out[#out + 1] = '!style="width:40%"|'
local lastKey
for _, m in ipairs(matches) do
local key
if m.date then
key = string.format("%04d-%02d-%02d", m.date.year, m.date.month, m.date.day)
else
key = "__" .. tostring(m.dateStr)
end
if key ~= lastKey then
out[#out + 1] = "|-"
out[#out + 1] = '|style="text-align:left"|' .. (m.dateStr or "")
lastKey = key
end
out[#out + 1] = '|- style="font-size:90%"'
out[#out + 1] = string.format("|%s||%s||%s||%s",
m.home or "", m.score or "", m.away or "", m.venue or "")
end
out[#out + 1] = "|}"
return table.concat(out, "\n")
end
function p.main(frame)
local args = frame.args or {}
if (not args.article or args.article == "") and frame.getParent then
local parent = frame:getParent()
if parent and parent.args then
args.article = (args.article and args.article ~= "") and args.article or parent.args.article
args[1] = args[1] or parent.args[1]
end
end
local articleTitle = args.article or args[1]
if not articleTitle or trim(articleTitle) == "" then
articleTitle = mw.title.getCurrentTitle().prefixedText
end
local title = mw.title.new(articleTitle)
if not title or not title.exists then
return '<strong class="error">Article not found: ' .. tostring(articleTitle) .. "</strong>"
end
local wikitext = title:getContent()
if not wikitext then
return '<strong class="error">Could not read content of: ' .. tostring(articleTitle) .. "</strong>"
end
local bodies = findFootballBoxInvocations(wikitext)
local matches = {}
for i, body in ipairs(bodies) do
local params = parseTemplateBody(body)
matches[#matches + 1] = buildMatchRecord(params, i)
end
if #matches == 0 then
return "<em>No football box matches found in " .. articleTitle .. ".</em>"
end
sortMatches(matches)
local wikitable = buildTable(matches)
return frame:preprocess(wikitable)
end
p._internal = {
trim = trim,
stripQuotes = stripQuotes,
findTemplateEnd = findTemplateEnd,
splitTopLevelPipes = splitTopLevelPipes,
parseTemplateBody = parseTemplateBody,
extractLabeledSection = extractLabeledSection,
extractHeaderSection = extractHeaderSection,
identifyTransclusion = identifyTransclusion,
findFootballBoxInvocations = findFootballBoxInvocations,
findNestedTemplate = findNestedTemplate,
formatDate = formatDate,
parseDateParam = parseDateParam,
buildScoreOutput = buildScoreOutput,
buildMatchRecord = buildMatchRecord,
sortMatches = sortMatches,
buildTable = buildTable,
}
return p