Module:Sandbox/User:Waddie96/table

From Wikipedia, the free encyclopedia
Jump to navigation Jump to search

--- @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