Module:TableLua
Appearance
This documentation is transcluded from Module:TableLua/doc.
Lua interface for building a table.
require( 'strict' )
--- Simplified Lua interface for building a table.
--- Using similar approach to the Codex Table component.
--- @class TableProps
--- @field caption string
--- @field hideCaption? boolean @default false
--- @field columns? TableColumn[] @default {}
--- @field data? TableRow[] @default {}
--- @field sort? TableSort @default {}
--- @field class? string @default ''
--- @field emptyState? string @default 'No data'
--- @class TableColumn
--- @field id string
--- @field label? string
--- @field textAlign? 'start'|'center'|'end'|'number' @default 'start'
--- @field width? string
--- @field minWidth? string
--- @field allowSort? boolean
--- A cell that sorts on something other than what it shows.
---
--- A sortable column is otherwise ordered by the text of its cells, which is
--- wrong wherever the text is a label for a scale: a rarity column sorted on
--- its badges runs Ascendant, Common, Mega Prime. Such a cell states the value
--- it should be compared on, which both this module's own sort and the
--- browser's read in preference to the cell's text.
---
--- @class TableCellProps
--- @field value string
--- @field sortValue? string|number
--- @alias TableCell string|TableCellProps
--- @alias TableRow TableCell[]
--- @alias TableSort table<string, TableSortOption>
--- @alias TableSortOption 'none'|'asc'|'desc'
local p = {}
---@param props TableProps
---@return TableProps
local function normalizeProps( props )
props.columns = props.columns or {}
props.data = props.data or {}
props.sort = props.sort or {}
props.emptyState = props.emptyState or 'There is no data available'
return props
end
--- @return TableRow[]
local function getEmptyStateData( emptyState )
return {
{ emptyState }
}
end
---@param root mw.html
---@param props TableProps
local function renderCaption( root, props )
if not props.hideCaption then
root
:tag( 'caption' )
:wikitext( props.caption )
end
end
---@param root mw.html
---@param props TableProps
local function renderHeader( root, props )
local headerRow = root:tag( 'tr' )
for _, column in ipairs( props.columns ) do
local th = headerRow:tag( 'th' )
th:attr( 'scope', 'col' )
if column.textAlign then
th:addClass( 't-table__cell--align-' .. column.textAlign )
end
if column.minWidth then
th:css( 'min-width', column.minWidth )
end
if column.width then
th:css( 'width', column.width )
end
if column.allowSort == false then
th:addClass( 'unsortable' )
end
th:wikitext( column.label or '' )
end
end
---@param root mw.html
---@param props TableProps
local function renderBody( root, props )
for _, row in ipairs( props.data ) do
local tr = root:tag( 'tr' )
for i, cell in ipairs( row ) do
local td = tr:tag( 'td' )
local column = props.columns[i]
if column and column.textAlign then
td:addClass( 't-table__cell--align-' .. column.textAlign )
end
if type( cell ) == 'table' then
if cell.sortValue ~= nil then
td:attr( 'data-sort-value', cell.sortValue )
end
td:wikitext( cell.value )
else
td:wikitext( cell )
end
end
end
end
--- What a cell is compared on.
---
--- @param cell TableCell
--- @return any
local function getSortable( cell )
if type( cell ) ~= 'table' then
return cell
end
if cell.sortValue ~= nil then
return cell.sortValue
end
return cell.value
end
--- Reduce a cell to the text it will end up displaying, so that sorting
--- compares content rather than markup.
---
--- Cells built from other modules carry parser strip markers: opaque
--- \127-delimited placeholders standing in for extension tags such as the
--- templatestyles an icon link emits. Left in place they sort as their
--- sequence number, which silently orders the column by insertion instead.
---
--- @param value string
--- @return string
local function cleanValue( value )
return ( value
:gsub( '\127[^\127]*\127', '' )
:gsub( '<[^>]+>', '' ) )
end
--- Sort the data according to TableSort
--- MW does not sort tables by default, so we need to sort them manually.
---
---@param props TableProps
local function sortData( props )
local colMap = {}
for i, column in ipairs( props.columns ) do
colMap[column.id] = i
end
local sortKeys = {}
for k in pairs( props.sort ) do
table.insert( sortKeys, k )
end
table.sort( sortKeys )
local function compare( valA, valB )
if valA == valB then return 0 end
if valA == nil then return -1 end
if valB == nil then return 1 end
local typeA, typeB = type( valA ), type( valB )
if typeA == 'number' and typeB == 'number' then
return valA < valB and -1 or 1
end
if typeA == 'string' and typeB == 'string' then
-- Attempt to sort numerically if the string contains a number,
-- even if it's wrapped in HTML.
local cleanA = cleanValue( valA )
local cleanB = cleanValue( valB )
-- Try to extract numbers for sorting.
local matchA = cleanA:match( '(-?%d+%.?%d*)' )
local matchB = cleanB:match( '(-?%d+%.?%d*)' )
if matchA and matchB then
local numA = tonumber( matchA )
local numB = tonumber( matchB )
-- Only use numeric sort if both are valid numbers and not equal.
if numA and numB and numA ~= numB then
return numA < numB and -1 or 1
end
end
-- Fallback to alphanumeric sort on cleaned text
if cleanA == cleanB then return 0 end
return cleanA < cleanB and -1 or 1
end
if typeA ~= typeB then
valA, valB = tostring( valA ), tostring( valB )
end
return valA < valB and -1 or 1
end
table.sort( props.data, function ( a, b )
if a == b then return false end
if b == nil then return true end
if a == nil then return false end
for _, colId in ipairs( sortKeys ) do
local order = props.sort[colId]
if order and order ~= 'none' then
local colIndex = colMap[colId]
if colIndex then
local result = compare(
getSortable( a[colIndex] ),
getSortable( b[colIndex] )
)
if result ~= 0 then
return (order == 'asc' and result == -1) or
(order == 'desc' and result == 1)
end
end
end
end
return false
end )
end
--- @param props TableProps
--- @return string
function p.render( props )
props = normalizeProps( props )
if props.data == {} then
props.data = getEmptyStateData( props.emptyState )
end
local root = mw.html.create( 'table' )
root
:addClass( 't-table' )
:addClass( 'wikitable' )
:addClass( props.class )
if next( props.sort ) then
root:addClass( 'sortable' )
sortData( props )
end
renderCaption( root, props )
renderHeader( root, props )
renderBody( root, props )
return mw.getCurrentFrame():extensionTag {
name = 'templatestyles', args = { src = 'Module:TableLua/styles.css' }
} .. tostring( root )
end
return p