Module:Sandbox/User:Waddie96/table
Jump to navigation
Jump to search
colspan = 2 | A
rowspan = 2 | F
| B | ||
| C | D | E |
| G | H | |
| I | J |
Usage
[edit source]{{#invoke:Sandbox/User:Waddie96/table|function_name}}
--- @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