Module:Table
---@module 'Table'
--- 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 _M = {}
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
local _tonumber = tonumber
--- Internal cache for strings that have already been trimmed
local trim_cache = {}
--- Internal reference of what strings are to be considered whitespace for wikitext conversion
local whitespace = {
[' '] = true,
['\n'] = true,
['\t'] = true,
['\r'] = true,
}
---
--- Error logging
---
---@param msg string Error message to log
---@param where 'console'|'preview' Where to log the error: 'console' or 'preview'
---@return nil
local function add_error(msg, where)
if where == 'console' then
mw.log('Module:Table error: ' .. msg)
elseif where == 'preview' then
mw.addWarning('<span class="error"><strong>[[Module:Table]] error:</strong> ' .. msg .. '</span>')
end
return nil
end
---
--- Protected call utility
---
---@param fn function Function to call
---@param ... any Arguments to pass to the function
---@return any|nil output Result of the function call, or `nil` if an error occurred
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 then
return output
else
return nil
end
end
---
---Convert to integer >= 0
---
---@param input any Input to convert
---@return integer|nil integer Non-negative integer or `nil` if invalid
function _M.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
---
--- Finds first non-whitespace character in a string
---
---@param s string Input string
---@param len integer Length of the string
---@return integer|nil index Index of the first non-whitespace character, or `nil`
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
---
--- Finds last non-whitespace character in a string
---
---@param s string Input string
---@param len integer Length of the string
---@return integer|nil index Index of the last non-whitespace character, or `nil`
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
---
--- Trims leading and trailing whitespace from a string
---
---@param s any Input string
---@return string trimmed Trimmed string
function _M.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 _M.cheap_trim(input)
if trim_cache[input] then
return trim_cache[input]
end
local trimmed = _M.trim_whitespace(input)
trim_cache[input] = trimmed
return trimmed
end
---
--- Parse a single cell
---
function _M.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 = _M.cheap_trim(cell.text)
return cell
end
-- Extract tables from wikitext safely
function _M.get_tables(wikitext)
local tables = {}
wikitext = '\n' .. wikitext
for t in _gmatch(wikitext, '\n{|.-\n|}') do
table_insert(tables, _M.cheap_trim(t))
end
return tables
end
---
---Get table by ID attribute
---
---@param wikitext string
---@param id string
---@return string|nil wikitext Wikitext with the specified ID, or `nil` if not found
function _M.get_table_by_id(wikitext, id)
local value
local tables = _M.get_tables(wikitext)
for _, t in ipairs(tables) do
local value = _match(t, "^{|[^\n]*id *= *[\"']?([^\"'\n]+)[\"']?[^\n]*\n")
if not value then
value = _match(t, "^{|[^\n]*id *= *[\'']?([^\''\n]+)[\'']?[^\n]*\n")
end
if value == id then
return t
end
end
return nil
end
---
---Parse table wikitext into structured data
---
---@param table_wikitext any
---@return table table_data Table data as a list of rows, each containing a list of cell objects
function _M.get_table_data(table_wikitext)
local table_data = {}
local text = _M.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, _M.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
---Accounts for colspan and rowspan, fills in `nil` for empty slots.
---
---@param table_data table Table data as returned by `get_table_data()`
---@return table slots 2D array representing the slot grid with merged cells accounted for
function _M.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 into wikitable syntax
---Preserves merged cell logic, skips nil slots, only outputs each cell once,
---can add styles, and set `colspan` and `rowspan` per cell.
---
---@param slots table Slot grid as returned by `get_table_slots()`
---@param cell_class_function? function Optional function to generate additional attributes for each cell. It should accept three parameters: the cell `object`, its row index (`y_axis`), and its column index (`x_axis`). It should return a string of additional attributes (e.g., `'class="my-class" style="color: red;"'`) or an empty string `''` if no additional attributes are needed.
---@param table_class? string Optional class(es) attribute for the entire table (default: `wikitable`)
---@return string wikitext_table Wikitext table output
function _M.render_slots(slots, cell_class_function, table_class)
table_class = table_class or 'wikitable'
local output = { '{| class="' .. table_class .. '"' }
local used = {}
for y_axis, row in ipairs(slots) do
table_insert(output, '|-')
for x_axis, 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_axis, x_axis)
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(output, attr_str .. ' ' .. (cell.text or ''))
end
end
end
table_insert(output, '|}')
return table.concat(output, '\n')
end
---
--- Convenience: get slot grid by table ID
---
function _M.slots_from_wikitext_by_id(wikitext, id)
local t = _M.get_table_by_id(wikitext, id)
if not t then
return nil
end
return _M.get_table_slots(_M.get_table_data(t))
end
---
--- Parses flat JSON object into structured table data
---
--- e.g. `[{"col1":"A","col2":"B"},{"col1":"C","col2":"D"}]`
--- into
--- ```lua
--- {
--- { {text="A", colspan=1, rowspan=1}, {text="B", colspan=1, rowspan=1} },
--- { {text="C", colspan=1, rowspan=1}, {text="D", colspan=1, rowspan=1} }
--- }
--- ```
---@param data table Flat JSON object as parsed by `parse_json()`
---@return table|nil table_data Structured table data or `nil` if invalid
local function flat_json_to_table(data)
if type(data) ~= 'table' then
add_error('Invalid data: must be a list of tables', 'preview')
return nil
end
-- If the first element is a table, assume it's already in the structured format
if #data > 0 and type(data[1]) == 'table' then
return data
end
local table_data = {}
for _, row in ipairs(data) do
if type(row) == 'table' then
local row_data = {}
-- Note: pairs() will iterate over string keys (like 'col1', 'col2')
for _, cell in pairs(row) do
table_insert(row_data, {
text = _tostring(cell),
colspan = 1,
rowspan = 1
})
end
table_insert(table_data, row_data)
else
add_error('Invalid row in flat JSON object removed. Must be a table: ' .. _tostring(row_data), 'preview')
-- This break is problematic if only one row is bad, but keeping original logic flow
break
end
end
-- FIX: Should return the constructed table_data, not an undefined 'slots'
return table_data
end
---
--- Parse JSON-style input safely
--- Will also parse if Lua table syntax is used instead of JSON
---
---@param json_data string JSON data to parse
---@return table|nil parsed_json_output Parsed JSON data or `nil` if invalid
function _M.parse_json(json_data)
-- First attempt: Strict JSON (standard mw.text.jsonDecode)
local ok_json, data = pcall(mw.text.jsonDecode, json_data)
if ok_json and type(data) == 'table' then
return data
end
-- Second attempt: Try to convert to loose Lua table/JSON syntax
-- 1. Replace single quotes with double quotes (handles 'string' -> "string")
local _json_data = _gsub(json_data, "'", '"')
-- 2. Quote unquoted keys (handles {key: value} and {key = value} -> {"key": value})
-- FIX: Use a character set [:=] to match both colon (JSON) and equals (Lua) delimiters
_json_data = _gsub(_json_data, '([%w_]+)%s*([:=])', '"%1"%2')
local ok_lua, data_lua = pcall(mw.text.jsonDecode, _json_data)
if ok_lua and type(data_lua) == 'table' then
return data_lua
end
-- If both fail, log the error with the original data
add_error('Invalid input: must be JSON or Lua table. Original input failed to parse.', 'preview')
return nil
end
--local tbl_data = data:gsub("'", '"'):gsub("([%w_]+)%s*:", '"%1":')
--if not ok or type(data) ~= 'table' then
-- add_error(mw.ustring.format(
-- 'Invalid JSON input. Input: %s. Output: %s.',
-- data,
-- tbl_data),
-- 'preview')
-- return nil
--end
--ok, tbl_data2 = pcall(mw.text.jsonDecode, tbl_data)
--if type(tbl_data2) == 'table' and ok then
-- return tbl_data2
--else
-- local ok2, tbl_data3 = pcall(mw.text.jsonDecode, json_data)
-- if ok2 and type(tbl_data3) == 'table' then
-- return tbl_data3
-- end
--end
--add_error('Invalid input: must be JSON or Lua table.', 'preview')
--return data
--end
---
--- Fetch and clean arguments passed as input by removing blank arguments,
--- trimming whitespace, and in wrapper templates only checking parentArgs
--- for efficiency.
---
---@param frame table `frame` object containing `args` field as received during invocation of the module with `{{#invoke:...}}` or from a template transclusion. Note, this will be a `table` object if called from another module.
---@return table<integer|string, any> clean_args Clean arguments after parsing with input in key-value pairs e.g. `{ param1 = 'value1', param2 = 'value2' }`
local function fetch_args(frame)
local getArgs = require('Module:Arguments').getArgs
if frame and frame.args then
return getArgs(frame, {
removeBlanks = true,
trim = true,
wrappers = { 'Template:Table', 'Template:Table/sandbox' }
})
else
add_error('No argument(s) passed to module.', 'preview')
return {}
end
end
---
--- Build table from `frame.args` in JSON format
---
--- #### Templates
-- Templates should use the `build()` function as an entry point.<br/>```{{#invoke: Table | build | ... }}```
--- #### Modules
--- Based on input:
--- - input is already a fully-expanded slot grid, use `render_slots()` directly.
--- - input is structured data (rows/cells), run it through `get_table_slots()` first.
--- - is a flat JSON object like `[{"col1":"A","col2":"B"}]`, use `parse_json()`.
---
---@param frame table Frame object with an ['args'] field, or table, containing the input.
---@return string wikitext_table Wikitext table output
function _M.build(frame)
local args = fetch_args(frame)
local data = args.data and _M.parse_json(args.data)
if not data then
return add_error(
mw.ustring.format(
'No output after argument(s) parsed as JSON. See [[Module:Table|template documentation]]. Input: %s. Output: %s.',
args.data or 'nil', data or 'nil'),
'preview')
end
local table_data = flat_json_to_table(data) or data
if not table_data then
return add_error(
mw.ustring.format(
'No output after JSON output parsed into table data. See [[Module:Table|template documentation]]. Input: %s and %s. Output: %s.',
args.data or 'nil', data or 'nil', table_data or 'nil'),
'preview')
end
local slots = _M.get_table_slots(table_data)
return _M.render_slots(slots)
end
return _M
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.