Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a230c281d8d277be

Jump to content

Module:Football group summary

From Wikipedia, the free encyclopedia
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