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.

Jump to content

Module:Sandbox/User:Waddie96/table

From Wikipedia, the free encyclopedia

--- @module WikitextTableBuilder
--- Parse and manipulate wikitext tables in MediaWiki.
---
--- Provides functions to extract tables, parse them into structured data,
--- and build a slot grid accounting for colspan/rowspan, classes, and styles.

local P = {}

-- Aliases for performance
local _gsub = mw.ustring.gsub
local _sub = mw.ustring.sub
local _match = mw.ustring.match
local _len = mw.ustring.len
local _gmatch = mw.ustring.gmatch
local _gsplit = mw.text.gsplit
local _tostring = tostring
local table_insert = table.insert

-- Trimming cache
local trim_cache = {}
local whitespace = { [" "] = true, ["\n"] = true, ["\t"] = true, ["\r"] = true }

-- Error logging
local function add_error(msg, where)
	if where == "console" then
		mw.log("WikitextTableBuilder error: " .. msg)
	elseif where == "preview" then
		mw.addWarning('<span class="error">WikitextTableBuilder error: ' .. msg .. "</span>")
	end
end

-- Protected call utility
local function try_call(fn, ...)
	local ok, output = xpcall(fn, function(err)
		add_error("Unexpected error in <code>try_call()</code>: " .. _tostring(err), "console")
	end, ...)
	if ok and type(output) == "table" then
		return output
	else
		return nil
	end
end

-- Convert to integer >= 0
function P.to_integer(input)
	local num = tonumber(input)
	if num and num >= 0 and math.floor(num) == num then
		return num
	end
	add_error("Expected non-negative integer but got: " .. _tostring(input), "console")
	return nil
end

-- Internal whitespace trimming
local function find_first_nonwhitespace(s, len)
	for i = 1, len do
		if not whitespace[_sub(s, i, i)] then
			return i
		end
	end
end

local function find_last_nonwhitespace(s, len)
	for i = len, 1, -1 do
		if not whitespace[_sub(s, i, i)] then
			return i
		end
	end
end

function P.trim_whitespace(s)
	local len = _len(s)
	local low = find_first_nonwhitespace(s, len)
	if not low then
		return ""
	end
	local high = find_last_nonwhitespace(s, len)
	if not high then
		add_error("Unexpected end in <code>trim_whitespace()</code> for input: " .. _tostring(s), "console")
		return ""
	end
	return _sub(s, low, high)
end

-- Cached fast trim
function P.cheap_trim(input)
	if trim_cache[input] then
		return trim_cache[input]
	end
	local trimmed = P.trim_whitespace(input)
	trim_cache[input] = trimmed
	return trimmed
end

-- Parse a single cell
function P.parse_cell(cell_wikitext)
	local cell = {}
	cell.colspan = tonumber(_match(cell_wikitext, 'colspan *= *"?([0-9]+)"?')) or 1
	cell.rowspan = tonumber(_match(cell_wikitext, 'rowspan *= *"?([0-9]+)"?')) or 1
	cell.text = _gsub(cell_wikitext, 'colspan *= *"?[0-9]+"?', "")
	cell.text = _gsub(cell.text, 'rowspan *= *"?[0-9]+"?', "")
	cell.text = P.cheap_trim(cell.text)
	return cell
end

-- Extract tables from wikitext safely
function P.get_tables(wikitext)
	local tables = {}
	wikitext = "\n" .. wikitext
	for t in _gmatch(wikitext, "\n{|.-\n|}") do
		table_insert(tables, P.cheap_trim(t))
	end
	return tables
end

-- Get table by ID attribute
function P.get_table_by_id(wikitext, id)
	for _, t in ipairs(P.get_tables(wikitext)) do
		local value = _match(t, "^{|[^\n]*id *= *[\"']?([^\"'\n]+)[\"']?[^\n]*\n")
		if value == id then
			return t
		end
	end
end

-- Parse table wikitext into structured data
function P.get_table_data(table_wikitext)
	local table_data = {}
	local text = P.cheap_trim(table_wikitext)

	text = _gsub(text, "^{|.-\n", "")
	text = _gsub(text, "\n|}$", "")
	text = _gsub(text, "^|%+.-\n", "")
	text = _gsub(text, "|%-.-\n", "|-\n")
	text = _gsub(text, "^|%-\n", "")
	text = _gsub(text, "\n|%-$", "")

	for row_wikitext in _gsplit(text, "|-", true) do
		local row_data = {}
		row_wikitext = _gsub(row_wikitext, "||", "\n|")
		row_wikitext = _gsub(row_wikitext, "!!", "\n|")
		row_wikitext = _gsub(row_wikitext, "\n!", "\n|")
		row_wikitext = _gsub(row_wikitext, "^!", "\n|")
		row_wikitext = _gsub(row_wikitext, "^\n|", "")

		for cell_wikitext in _gsplit(row_wikitext, "\n|") do
			if cell_wikitext ~= "" then
				table_insert(row_data, P.parse_cell(cell_wikitext))
			end
		end
		if #row_data > 0 then
			table_insert(table_data, row_data)
		end
	end
	return table_data
end

-- Build slot grid
function P.get_table_slots(table_data)
	if not table_data or type(table_data) ~= "table" then
		add_error("Invalid table: must be a table of rows", "console")
		return {}
	end

	local slots = {}

	for rowIndex, row in ipairs(table_data) do
		if type(row) ~= "table" then
			add_error("Invalid row at index " .. rowIndex .. ": must be a table of cells", "console")
		else
			for cellIndex, cell in ipairs(row) do
				if type(cell) ~= "table" then
					add_error("Invalid cell at row " .. rowIndex .. ", column " .. cellIndex, "console")
				else
					local rowspan = cell.rowspan or 1
					local colspan = cell.colspan or 1
					local x = cellIndex
					local y = rowIndex

					-- Skip occupied slots (from previous rowspan/colspan)
					while slots[y] and slots[y][x] do
						x = x + 1
					end

					-- Fill slots
					for dy = 0, rowspan - 1 do
						for dx = 0, colspan - 1 do
							while (y + dy) > #slots do
								table.insert(slots, {})
							end
							slots[y + dy][x + dx] = cell
						end
					end
				end
			end
		end
	end

	return slots
end

-- Render slot grid
function P.render_slots(slots, cell_class_function)
	local out = { '{| class="wikitable"' }
	local used = {}

	for y, row in ipairs(slots) do
		table.insert(out, "|-")
		for x, cell in ipairs(row) do
			if cell and not used[cell] then
				used[cell] = true
				local parts = {}

				if cell.rowspan and cell.rowspan > 1 then
					table.insert(parts, "rowspan = " .. tostring(cell.rowspan))
				end
				if cell.colspan and cell.colspan > 1 then
					table.insert(parts, "colspan = " .. tostring(cell.colspan))
				end

				if cell_class_function then
					local custom_attr = cell_class_function(cell, y, x)
					if custom_attr and custom_attr ~= "" then
						table.insert(parts, custom_attr)
					end
				end

				local attr_str = (#parts > 0) and (table.concat(parts, " ") .. " |") or "|"
				table.insert(out, attr_str .. " " .. (cell.text or ""))
			end
		end
	end

	table.insert(out, "|}")
	return table.concat(out, "\n")
end

-- Convenience: get slot grid by table ID
function P.slots_from_wikitext_by_id(wikitext, id)
	local t = P.get_table_by_id(wikitext, id)
	if not t then
		return nil
	end
	return P.get_table_slots(P.get_table_data(t))
end

-- Parse JSON input safely
function P.from_json(json_str)
	local ok, data = pcall(mw.text.jsonDecode, json_str)
	if not ok or type(data) ~= "table" then
		add_error("Invalid JSON input", "console")
		return nil
	end
	return data
end

-- Build table from frame args JSON
function P.build(frame)
	local args = frame.args
	local data = args.data and P.from_json(args.data)
	if not data then
		return ""
	end
	return P.render_slots(P.get_table_slots(data))
end

return P