Module:Data Table
Jump to navigation
Jump to search
This is the main module for the following templates:
In addition, this module exports the following functions.
Main
{{#invoke:Data Table|Main|game=|columns=|optionalColumns=}}
In addition to being the main invocation of Template:Data Table, this function can be used to create templates with fixed values for the parameters columns
and/or game
, e.g. Template:Goddess Cubes, Template:Gold Skulltulas, Template:Other Names, etc.
Parameters
Parameter | Status | Description |
---|---|---|
game | optional | See Template:Data Table |
columns | optional | See Template:Data Table |
optionalColumns | optional | See printTable |
parseRows
parseRows(cells)
Allows other templates to use Template:Data Table's table-like syntax. Used by Module:Wares, for example.
Returns
- A list of rows.
Examples
# | Input | Output |
---|---|---|
1 | parseRows({
"-",
"cell1",
"cell2",
"-",
"cell3",
"cell4",
"-",
})
| {
{
cells = {"cell1", "cell2"},
},
{
cells = {"cell3", "cell4"},
},
}
|
printTable
printTable(rows, args, [postFormatHook])
Parameters
args
- Template:Data Table arguments, plus
optionalColumns
for other table templates. [game]
columns
[optionalColumns]
- List of indices referring to optional columns. If multiple columns are marked as optional but some are provided in the table, columns are omitted in the order specified by this list.
[caption]
[responsiveModeEnabled]
[sortable]
[stretch]
[postFormatHook]
- Advanced usage - allows other modules to further customize a data table before printing it. Used by Module:Armor, for example.
Returns
- A data table.
Examples
# | Input | Result | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | printTable(
{
{
cells = {"Blue Chu Jelly", "Blue Potion"},
},
{
cells = {"Red Chu Jelly", "Red Potion"},
},
},
{
game = "TWWHD",
columns = {"Chu Jelly [Term]", "Potion [Term]"},
}
)
|
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
The optionalColumns property allows columns to be omitted | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | printTable(
{
{
cells = {"Treasure Chest", "Small Key"},
},
{
cells = {"Treasure Chest", "Red Rupee"},
},
},
{
game = "PH",
optionalColumns = {3},
columns = {
"Treasure Chest [Image:Model:50x50px][Term]",
"Contents [Image:Model:50x50px][Term]",
"Coordinates",
},
}
)
|
|
local p = {}
local h = {}
local Tags = require("Module:Data Table/Tags") -- edit this page to add new tags
local Franchise = require("Module:Franchise")
local TransclusionArguments = require("Module:Transclusion Arguments")
local utilsArg = require("Module:UtilsArg")
local utilsCargo = require("Module:UtilsCargo")
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")
local CATEGORY_INVALID_ARGS = require("Module:Constants/category/invalidArgs")
local SMALL_WIDTH_THRESHOLD = 40
local responsiveModeThresholds = { -- "mobile-friendly mode" is enabled when a table has:
minWidth = 40, -- a row exceeding ~40 characters in length
maxRows = 100, -- no more than 100 rows
}
function h.warn(msg)
local utilsError = require("Module:UtilsError")
utilsError.warn(msg)
end
function h.err(errMsg, warnMsg)
local utilsError = require("Module:UtilsError")
return utilsError.error(errMsg, warnMsg)
end
-- Other modules can extend this one by using extensionArgs
function p.Main(frame, extensionArgs)
extensionArgs = extensionArgs or {}
local frameArgs = utilsTable.merge({}, frame:getParent().args, frame.args)
utilsTable.merge(p.Templates["Data Table"].params, extensionArgs.params or {})
local args, err = utilsArg.parse(frameArgs, p.Templates["Data Table"])
local categories = err and err.categoryText or ""
args.columnNames = p.extractColumnTags(args.columns)
local rows = p.parseRows(args.cells)
local storeCategories = h.storeTable(args, extensionArgs.storeFn, rows)
categories = categories..storeCategories
local dataTable = p.printTable(rows, args)
if extensionArgs.requiredColumns then
local missingColumns = utilsTable.difference(extensionArgs.requiredColumns, args.columnNames)
if #missingColumns > 0 then
missingColumns = utilsTable.map(missingColumns, utilsMarkup.code)
h.warn(string.format("Columns %s are required.", mw.text.listToText(missingColumns)))
categories = categories.."[[Category:"..CATEGORY_INVALID_ARGS.."]]"
end
end
return dataTable, categories
end
function p.Copy(frame)
local args, err = utilsArg.parse(frame:getParent().args, p.Templates["Data Table Copy"])
local categories = err and err.categoryText or ""
categories = categories.."[[Category:Pages with data tables]]"
if not args.fromPage or not args.storedAs then
return "", categories
end
local dataTable, copyCategories = h.copy(args)
TransclusionArguments.store({
module = "Module:Data Table",
isValid = copyCategories == "",
args = args,
})
return dataTable, categories..copyCategories
end
function p.ListExtensions(frame)
local utilsPage = require("Module:UtilsPage")
local subpages = utilsPage.getSubpages("Template:Data Table")
local extensions = utilsTable.filter(subpages, function(subpage)
return not (string.find(subpage, "/Documentation$") or string.find(subpage, "/Styles.css") or string.find(subpage, "/Store"))
end)
extensions = utilsTable.map(extensions, utilsMarkup.link)
extensions = utilsMarkup.bulletList(extensions)
return extensions
end
function p.parseRows(cells)
local rows = {}
local currentRow = {
cells = {}
}
for i, cellText in ipairs(cells or {}) do
local isDivider = string.find(cellText, "^%-+$") -- is a divider if it contains only -'s
if isDivider and i ~= 1 then
table.insert(rows, currentRow)
currentRow = {
cells = {},
}
elseif not isDivider then
table.insert(currentRow.cells, cellText)
end
end
if #currentRow.cells > 0 then
table.insert(rows, currentRow)
end
return rows
end
function p.printTable(rows, args, postFormatHook)
local tableArgs, categories = h.formatTable(rows, args)
categories = categories.."[[Category:Pages with data tables]]"
if postFormatHook then
tableArgs = postFormatHook(tableArgs)
end
local styles = h.styles({ "Template:Data Table/Styles.css" })
local incompleteNotice = isMainspace and h.incomplete(tableArgs) or ""
local tableNotes = mw.getCurrentFrame():expandTemplate({ title = "List Notes" })
local desktopTable = h.desktopTable(tableArgs)
local mobileTable = h.mobileTable(tableArgs)
-- Mobile table has to come first otherwise template styles generated from tags are not loaded correctly on mobile due to the "nomobile" class
return styles..incompleteNotice..mobileTable..desktopTable..tableNotes..categories
end
function p.extractColumnTags(columns)
local columnHeaders = {}
local columnTagsByColumn = {}
local colspans
for i, column in ipairs(columns) do
local columnTags = {}
local matches
repeat
column, matches = string.gsub(column, "%s*%[([^%]]+)%]$", function(tag)
tag = utilsString.split(tag, ":")
if tag[1] == "Colspan" then
colspans = colspans or {}
colspans[i] = {
colspan = tonumber(tag[2]),
}
else
table.insert(columnTags, {
name = tag[1],
args = utilsTable.tail(tag),
})
end
return ""
end)
until matches == 0
columnTags = utilsTable.reverse(columnTags)
if not (colspans and colspans[i]) then
table.insert(columnTagsByColumn, columnTags)
table.insert(columnHeaders, column)
elseif colspans then
colspans[i].header = column
end
end
return columnHeaders, columnTagsByColumn, colspans
end
function h.storeTable(args, storeFn, rows)
local categories = ""
if args.storeAs then
h.store(args, rows)
end
if storeFn then
local storeCategories = h.customStore(args, storeFn, rows)
if storeCategories then
categories = categories..storeCategories
end
end
if args.storeAs or storeFn then
categories = categories..utilsCargo.categoryStoring()
end
return categories
end
function h.store(args, rows)
local frame = mw.getCurrentFrame()
local columns = utilsTable.map(args.columns, function(column)
column = mw.text.killMarkers(column)
-- remove tags that shouldn't be applied to Data Table Copies
for tagName, tag in ipairs(Tags.attributeTags) do
if tag.noCopy then
column = string.gsub(column, "%["..tagName.."[^%]]+]", "")
end
end
return column
end)
columns = table.concat(columns, ", ")
frame:expandTemplate({
title = "Data Table/Store",
args = {
game = args.game,
tableName = args.storeAs,
columns = columns
}
})
for i, row in ipairs(rows) do
for j, cell in ipairs(row.cells) do
frame:expandTemplate({
title = "Data Table/Store",
args = {
game = args.game,
tableName = args.storeAs,
rowIndex = i,
columnIndex = j,
cell = mw.text.unstrip(cell),
}
})
end
end
end
function h.customStore(args, storeFn, rows)
local tableData = {}
for i, row in ipairs(rows) do
local rowData = {}
for j, cell in ipairs(row.cells) do
local columnName = args.columnNames[j]
rowData[columnName] = cell
end
table.insert(tableData, rowData)
end
return storeFn(args, tableData)
end
function h.copy(args, filterFn)
local columnsQuery, categories = utilsCargo.query("DataTables", "columns", {
where = utilsCargo.allOf({
_pageName = args.fromPage,
tableName = args.storedAs,
}, "columns HOLDS LIKE '%'"),
limit = 1
})
if #columnsQuery == 0 then
local err = h.err("Data not found", string.format("No table <code>%s</code> is stored on page [[%s]]", args.storedAs, args.fromPage))
return err, categories.."[[Category:"..CATEGORY_INVALID_ARGS.."]]"
end
local headers = utilsString.split(columnsQuery[1].columns)
headers = utilsTable.map(headers, mw.text.killMarkers)
-- Here we use "columns" to refer to actual data columns (excluding colspans)
-- We use "headers" to refer to any table header (including colspans)
local columnNames, tags, colspans = p.extractColumnTags(headers)
local headerNames = utilsTable.clone(columnNames)
for k, colspan in pairs(colspans or {}) do
table.insert(headerNames, k, colspan.header)
end
local columnLookup = utilsTable.invert(columnNames)
local headerLookup = utilsTable.invert(headerNames)
local includedColumnIndices = {}
local includedHeaderIndices = {}
if not args.columns then
includedColumnIndices = utilsTable.keys(columnNames)
includedHeaderIndices = utilsTable.keys(headerNames)
end
local copyHeaderNames, copyHeaderTags = p.extractColumnTags(args.columns or args.excludeColumns or {})
for i, copyHeader in ipairs(copyHeaderNames) do
copyHeader = mw.text.killMarkers(copyHeader)
local columnIndex = columnLookup[copyHeader]
local headerIndex = headerLookup[copyHeader]
if not headerIndex then
h.warn(string.format("Column <code>%s</code> does not exist in table <code>%s</code> stored on page [[%s]]. The columns defined for this table are as follows: %s", copyHeader, args.storedAs, args.fromPage, utilsMarkup.bulletList(headerNames)))
categories = categories.."[[Category:"..CATEGORY_INVALID_ARGS.."]]"
includedColumnIndices = utilsTable.keys(columnNames)
includedHeaderIndices = utilsTable.keys(headerNames)
break
elseif args.columns then
table.insert(includedHeaderIndices, headerIndex)
if columnIndex then
table.insert(includedColumnIndices, columnIndex)
end
elseif args.excludeColumns then
includedHeaderIndices[headerIndex] = nil
if columnIndex then
includedColumnIndices[columnIndex] = nil
end
end
end
includedColumnIndices = utilsTable.compact(includedColumnIndices)
includedHeaderIndices = utilsTable.compact(includedHeaderIndices)
local whereExpressions = utilsTable.compact({
string.format("_pageName = '%s'", utilsCargo.escape(args.fromPage)),
args.storedAs and string.format("tableName = '%s'", utilsCargo.escape(args.storedAs)),
"columns HOLDS NOT LIKE '%'",
})
local whereClause = table.concat(whereExpressions, " AND ")
local rowsQuery = utilsCargo.query("DataTables", "game, rowIndex, columnIndex, cell", {
where = whereClause,
limit = 5000,
orderBy= "rowIndex, columnIndex",
})
local rows = {}
local currentRow = { cells = {} }
local previousColumn = 0
for i, rowData in ipairs(rowsQuery) do
local columnIndex = tonumber(rowData.columnIndex)
if columnIndex < previousColumn then
table.insert(rows, currentRow)
currentRow = { cells = {} }
end
table.insert(currentRow.cells, rowData.cell or " ")
previousColumn = columnIndex
end
table.insert(rows, currentRow)
if args.rowsWith or args.rowsExcluding then
rows = utilsTable.filter(rows, function(row)
local includeRow = args.rowsWith == nil
local excludeRow = false
for i, cell in ipairs(row.cells) do
if args.rowsWith and string.find(cell, args.rowsWith) then
includeRow = true
end
if args.rowsExcluding and string.find(cell, args.rowsExcluding) then
excludeRow = true
end
end
return includeRow and not excludeRow
end)
end
if #rows == 0 then
local withCriteria = args.rowsWith and string.format("<code>rowsWith= %s</code>", args.rowsWith)
local excludingCriteria = args.rowsExcluding and string.format("<code>rowsExcluding= %s</code>", args.rowsExcluding)
local critera = withCriteria or excludingCriteria
if withCriteria and excludingCriteria then
critera = withCritera.." and "..excludingCriteria
end
local criteriaMsg = criteria and " matching criteria "..criteria or ""
local err = h.err("Data not found", string.format("No rows%s were found in table <code>%s</code> on page [[%s]]", criteriaMsg, args.storedAs, args.fromPage))
return err, categories.."[[Category:"..CATEGORY_INVALID_ARGS.."]]"
end
if filterFn then
rows = utilsTable.filter(rows, filterFn)
end
local includedHeaders = {}
for i, headerIndex in ipairs(includedHeaderIndices) do
local header = headers[headerIndex]
local copyTags = copyHeaderTags[i]
local headerAlias = copyTags and copyTags[1] and copyTags[1].name == "As" and copyTags[1].args[1]
if headerAlias then
local headerName = headerNames[headerIndex]
header = string.gsub(header, "^"..headerName, headerAlias)
end
table.insert(includedHeaders, header)
end
for i, row in ipairs(rows) do
local rowCells = {}
for j, columnIndex in ipairs(includedColumnIndices) do
table.insert(rowCells, row.cells[columnIndex])
end
row.cells = rowCells
end
local dataTable = p.printTable(rows, {
game = rowsQuery[1].game,
columns = includedHeaders,
responsiveModeEnabled = args.responsiveModeEnabled,
})
return dataTable, categories
end
function h.formatTable(rows, args)
local categories = ""
local maxRowSize = h.maxRowSize(rows)
local columns = utilsTable.clone(args.columns)
local omittedColumns = #columns - maxRowSize
for i = 1, omittedColumns do
local columnIndex = args.optionalColumns and args.optionalColumns[i]
table.remove(columns, columnIndex)
end
local columnHeaders, columnTags, colspans = p.extractColumnTags(columns)
-- Data rows
local unsortableColumns = {}
local hasEmptyCells = false
local width = 0
local maxRowSize = 0
for rowIndex, row in ipairs(rows) do
row.width = 0
row.cells = row.cells or row
maxRowSize = math.max(maxRowSize, #row.cells)
for columnIndex, cellText in ipairs(row.cells) do
local tags = columnTags[columnIndex] or {}
local isLastRow = #rows == rowIndex -- needed for Rowspan tag
local cell, errCategories = h.formatCell(cellText, args, tags, isLastRow, columnIndex)
categories = categories..errCategories
cell.content = tostring(cell.content)
cell.columnHeader = columnHeaders[columnIndex]
row.cells[columnIndex] = cell
if cell.sortValue == false then
unsortableColumns[columnIndex] = true
end
if cell.id and not row.id then
row.id = cell.id
end
if cell.isEmpty then
hasEmptyCells = true
end
row.width = row.width + (cell.size or 0)
end
width = math.max(width, row.width)
end
-- Header rows
local mainHeaders = {}
local subHeaders = {}
local i = 1
while i <= #columnHeaders do
local colspan = colspans and colspans[i]
if colspan then
table.insert(mainHeaders, {
content = colspan.header,
colspan = colspan.colspan,
unsortable = true,
})
for j = 0, (colspan.colspan - 1) do
table.insert(subHeaders, {
content = columnHeaders[i],
unsortable = unsortableColumns[i],
})
i = i + 1
end
else
table.insert(mainHeaders, {
content = columnHeaders[i],
rowspan = colspans and 2 or nil,
unsortable = unsortableColumns[i],
})
i = i + 1
end
end
local responsiveModeEnabled = args.responsiveModeEnabled
if responsiveModeEnabled == nil then
local title = mw.title.getCurrentTitle()
local isMainspace = title.nsText == "" or utilsTable.includes({"Data Table", "Data Table Copy", "Wares"}, title.rootText) -- These templates are treated as mainspace for testing purposes
-- For the reasoning behind these conditions, see [[Template:Data Table/Documentation]] responsiveModeEnabled parameter
responsiveModeEnabled = isMainspace and width > responsiveModeThresholds.minWidth and #rows < responsiveModeThresholds.maxRows
end
local tableArgs = {
game = args.game,
storeAs = args.storeAs,
mainHeaders = mainHeaders,
subHeaders = subHeaders,
rows = rows,
caption = args.caption,
sortable = args.sortable ~= false and #rows > 3,
stretch = args.stretch and args.stretch ~= "false",
vertical = args.vertical,
responsiveModeEnabled = responsiveModeEnabled,
hasEmptyCells = hasEmptyCells,
isMissingRows = #rows == 0,
}
return tableArgs, categories
end
function h.maxRowSize(rows)
local rowSizes = utilsTable.map(rows, function(row)
return #(row.cells or {})
end)
return utilsTable.max(rowSizes) or 0
end
function h.formatCell(cellText, args, tags, isLastRow, columnIndex)
local cell = {
raw = cellText,
class = "data-table__cell",
content = mw.html.create("div"),
isLastRow = isLastRow,
columnIndex = columnIndex,
}
if type(cellText) == "table" then -- allows modules like Module:Wares to pass in cells as objects for things like custom sort values
cell = utilsTable.merge(cell, cellText)
cell.raw = cellText.content
cell.content = mw.html.create("div")
end
local categories = ""
local contentTags = utilsTable.filter(tags, function(tag)
return Tags.contentTags[tag.name]
end)
local attributeTags = utilsTable.filter(tags, function(tag)
return Tags.attributeTags[tag.name]
end)
local unrecognizedTags = utilsTable.filter(tags, function(tag)
return not Tags.contentTags[tag.name] and not Tags.attributeTags[tag.name]
end)
for _, tag in ipairs(contentTags) do
h.applyContentTag(cell, tag, args)
-- a cludge to fix a bug where N/A cells with multiple tags get multiple N/A symbols in output
if string.find(cell.raw, "^N/A") then
break
end
end
if #contentTags == 0 then
h.applyContentTag(cell, {}, args)
end
for _, tag in ipairs(attributeTags) do
cell.rowIndex = rowIndex
h.applyAttributeTag(cell, tag, args)
end
for i, tag in ipairs(unrecognizedTags) do
h.warn(string.format("Unrecognized tag <code>%s</code>", tag.name))
categories = categories.."[[Category:"..CATEGORY_INVALID_ARGS.."]]"
end
return cell, categories
end
function h.applyContentTag(cell, tag, args)
local tagName = tag.name or ""
if cell.raw == "" then
tagName = "EmptyCell"
cell.isEmpty = true
elseif string.find(cell.raw, "^N/A") then
tagName = "NotApplicable"
end
local formatter = Tags.contentTags[tagName] and Tags.contentTags[tagName].formatter
if type(formatter) ~= "function" then
return
end
local cellData = formatter(cell.raw, args, tag.args, args)
cell.content
:addClass("data-table__cell-content")
:tag("div")
:addClass("data-table__cell-content-text")
:wikitext(cellData.text)
:done()
if tagName ~= "" then
cell.content:addClass("data-table__cell-content--"..utilsString.kebabCase(tagName))
end
if cell.raw == "" then
cell.content:addClass("data-table__cell-content--empty")
end
if cellData.sortValue ~= nil then
cell.sortValue = cellData.sortValue
end
cell.size = math.max(cell.size or 0, cellData.size or 1000) -- assume by default the cell is large to be on the safe side - better to overestimate than underestimate
end
function h.applyAttributeTag(cell, tag, args)
local formatter = Tags.attributeTags[tag.name] and Tags.attributeTags[tag.name].formatter
if type(formatter) ~= "function" then
return
end
formatter(cell, args, tag.args)
end
-- With wide tables we have no choice but to show something completely different on mobile
-- However, we don't need to do this for tables which we know are small enough to fit on mobile screens
function h.desktopTable(args)
if args.vertical then
return ""
end
-- Exclude cells which are rowspanned over
local tableArgs = mw.clone(args)
for i, row in ipairs(tableArgs.rows) do
local cells = {}
for j, cell in ipairs(row.cells) do
if not cell.skip then
table.insert(cells, cell)
end
end
tableArgs.rows[i].cells = cells
end
table.insert(tableArgs.rows, 1, { cells = args.mainHeaders, header = true})
if #args.subHeaders > 0 then
table.insert(tableArgs.rows, 2, { cells = args.subHeaders, header = true})
end
local wikitable = utilsLayout.table(tableArgs)
local html = mw.html.create("div")
:addClass("data-table data-table--desktop")
:addClass(args.responsiveModeEnabled and "size-large-up nomobile" or nil) --nomobile prevents the desktop table HTML from being loaded on the mobile skin, saving data and load time
:wikitext(wikitable)
return tostring(html)
end
function h.mobileTable(args)
if not args.responsiveModeEnabled and not args.vertical then
return ""
end
local html = mw.html.create("div")
:addClass("data-table data-table--mobile")
:addClass(not args.vertical and "size-medium-down" or nil)
:tag("table")
:addClass("wikitable")
for i, row in ipairs(args.rows) do
local subHeaderIndex = 1
if i ~= 1 then
html:tag("tr")
:addClass("data-table__separator")
end
local hasSubheaders = #args.subHeaders > 0
local subHeaderIndex = 1
local columnIndex = 1
for mainHeaderIndex = 1, #args.mainHeaders do
local header = args.mainHeaders[mainHeaderIndex] or {}
local colspan = header.colspan == nil and hasSubheaders and 2 or nil
local tableRow = html:tag("tr")
:tag("th")
:addClass("data-table__row-header")
:attr("rowspan", header.colspan)
:attr("colspan", colspan)
:wikitext(header.content)
:done()
if header.colspan then
for i = 1, header.colspan do
if i ~= 1 then
tableRow = tableRow:tag("tr")
end
local cell = row.cells[columnIndex] or {}
local subheader = args.subHeaders[subHeaderIndex] or {}
tableRow:tag("th")
:addClass("data-table__row-header")
:wikitext(subheader.content)
:done()
:tag("td")
:addClass(cell.class)
:css(cell.styles or {})
:wikitext(cell.content)
:done()
subHeaderIndex = subHeaderIndex + 1
columnIndex = columnIndex + 1
end
else
local cell = row.cells[columnIndex] or {}
tableRow:tag("td")
:addClass(cell.class)
:css(cell.styles or {})
:wikitext(cell.content)
:done()
columnIndex = columnIndex + 1
end
end
end
return tostring(html:allDone())
end
function h.incomplete(args)
local addMissingRows = args.isMissingRows and "add the missing rows" or nil
local fillEmptyCells = args.hasEmptyCells and "fill in the empty cells" or nil
local addDataAction = addMissingRows or fillEmptyCells
if addMissingRows and fillEmptyCells then
addDataAction = addMissingRows.." and "..fillEmptyCells
end
if not addDataAction then
return ""
end
local editPage = '<span class="plainlinks">[{{fullurl:{{FULLPAGENAME}}|action=edit}} editing the page]</span>'
local message = string.format("<hr><i>The following data table is incomplete. You can help {{SITENAME}} by %s to %s.</i><hr>", editPage, addDataAction)
message = mw.getCurrentFrame():preprocess(message)
message = message.."[[Category:Articles with incomplete data]]"
local game = args.game
game = game and Franchise.baseGame(game)
game = game and Franchise.shortName(game)
if game then
message = message..string.format("[[Category:%s articles with incomplete data]]", game)
end
return message
end
function h.styles(stylesheets)
local styles = ""
for i, stylesheet in ipairs(stylesheets) do
styles = styles..mw.getCurrentFrame():extensionTag({
name = "templatestyles",
args = { src = stylesheet },
})
end
return styles
end
function p.Schemas(frame)
return {
printTable = {
args = {
type = "record",
required = true,
desc = "[[Template:Data Table]] arguments, plus <code>optionalColumns</code> for other [[:Category:Table templates|table templates]].",
properties = {
{
name = "game",
type = "string"
},
{
name = "columns",
required = true,
type = "array",
items = { type = "string" },
},
{
name = "optionalColumns",
type = "array",
items = { type = "number" },
desc = "List of indices referring to optional columns. If multiple columns are marked as optional but some are provided in the table, columns are omitted in the order specified by this list.",
},
{
name = "caption",
type = "string",
},
{
name = "responsiveModeEnabled",
type = "boolean",
},
{
name = "sortable",
type = "boolean",
},
{
name = "stretch",
type = "boolean",
},
}
},
rows = {
required = true,
type = "array",
items = {
type = "record",
properties = {
{
name = "cells",
type = "array",
items = { type = "string" },
},
},
},
},
postFormatHook = {
type = "function",
desc = "Advanced usage - allows other modules to further customize a data table before printing it. Used by [[Module:Armor]], for example.",
},
},
}
end
function p.Documentation(frame)
return {
Main = {
desc = "In addition to being the main invocation of [[Template:Data Table]], this function can be used to create [[Category:Table templates|Data Table-like]] templates with fixed values for the parameters <code>columns</code> and/or <code>game</code>, e.g. [[Template:Goddess Cubes]], [[Template:Gold Skulltulas]], [[Template:Other Names]], etc.",
frameParamsOrder = {"game", "columns", "optionalColumns"},
frameParams = {
game = {
desc = "See [[Template:Data Table]]",
},
columns = {
desc = "See [[Template:Data Table]]",
},
optionalColumns = {
desc = "See [[#printTable|printTable]]",
},
},
},
parseRows = {
desc = "Allows other templates to use [[Template:Data Table]]'s table-like syntax. Used by [[Module:Wares]], for example.",
params = {"cells"},
returns = {
"A list of rows."
},
cases = {
outputOnly = true,
{
args = {
{"-", "cell1", "cell2", "-", "cell3", "cell4", "-"}
},
},
},
},
printTable = {
params = {"rows", "args", "postFormatHook"},
returns = "A data table.",
cases = {
resultOnly = true,
{
args = {
{
{
cells = {"Blue Chu Jelly", "Blue Potion"}
},
{
cells = {"Red Chu Jelly", "Red Potion"},
},
},
{
game = "TWWHD",
columns = {"Chu Jelly [Term]", "Potion [Term]"}
},
},
},
{
desc = "The <code>optionalColumns</code> property allows columns to be omitted",
args = {
{
{
cells = {"Treasure Chest", "Small Key"},
},
{
cells = {"Treasure Chest", "Red Rupee"},
}
},
{
game = "PH",
columns = {"Treasure Chest [Image:Model:50x50px][Term]", "Contents [Image:Model:50x50px][Term]", "Coordinates"},
optionalColumns = {3},
},
},
},
},
},
}
end
local responsiveModeEnabledDoc = {
type = "boolean",
desc = 'Data tables have a responsive "mobile-friendly" mode which by default is turned on when:'
.."\n* "..string.format("<b>The template estimates the width of the widest row to be greater than %d characters.</b>", responsiveModeThresholds.minWidth)
.."\n* "..string.format("<b>The table has less than %d rows.</b>", responsiveModeThresholds.maxRows)
.."\n: Responsive mode generates a lot of HTML and therefore cannot be used with excessively long tables due to the risk of exceeding {{MediaWiki|Manual:$wgMaxArticleSize|$wgMaxArticleSize}}."
.."\n* <b>The page is in the main namespace.</b>"
.."\n: Data tables are sometimes used in template and module documentation. Since these pages are not reader-facing, page loading speed matters more than mobile support."
.."\n Responsive mode can be forced on or off by setting this parameter to <code>true</code> or <code>false</code>, respectively.",
trim = true,
nilIfEmpty = true,
}
p.Templates = {
["Data Table/Store"] = {
purpose = "Stores data into the [[Special:CargoTables/DataTables|DataTables]] Cargo table, for use by {{Template|Data Table Copy}}.",
storesData = true,
usage = "This template is transcluded by [[Module:Data Table]].",
},
["Data Table"] = {
format = "block",
purpose = "<p>Displays tabular data in a way that is mobile-friendly. Minimizes the amount of boilerplate wikitext that most {{SITENAME}} tables require. Automatically center-aligns columns and applies templates such as {{Template|Term}}, {{Template|Amounts}}, {{Template|Rupee}}, etc.</p><p>The table data can stored in [[Special:CargoTables/DataTables|Cargo]] so that the table (or a subset of its rows) can be displayed on other relevant pages using {{Template|Data Table Copy}}.",
boilerplate = {
separateRequiredParams = false,
},
paramOrder = {"game", "storeAs", "caption", "responsiveModeEnabled", "vertical", "sortable", "stretch", "columns", "..."},
params = {
game = {
type = "string",
suggested = true,
enum = Franchise.enum(),
desc = "A game code. Used to automatically apply game-based templates (e.g. {{Template|Term}}, {{Template|Amounts}}, {{Template|Rupee}}) to table cells.",
},
storeAs = {
type = "string",
desc = "<p>If present, the data is stored in the [[Special:CargoTables/DataTables|DataTables]] Cargo table under the given name. Other pages can retrieve the data by that name using {{Template|Data Table Copy}}.</p><p>When a page has multiple data tables, each <code>storeAs</code> value must be unique.</p>",
trim = true,
nilIfEmpty = true,
},
caption = {
type = "string",
desc = "A table caption.",
trim = true,
nilIfEmpty = true,
},
responsiveModeEnabled = responsiveModeEnabledDoc,
sortable = {
type = "boolean",
desc = "If set to <code>false</code>, the data table will not be sortable.",
trim = true,
},
stretch = {
type = "boolean",
desc = "If present and set to anything other than <code>false</code>, the data table will stretch to the full width of the page.",
},
vertical = {
type = "boolean",
desc = "Lays out the table columns vertically instead of horizontally"
},
columns = {
type = "content",
required = true,
desc = "Comma-separated list of column headers. One or more '''tag''' can be appended to each column to indicate how the template should handle data in that column. See [[#Tags]] below.",
trim = true,
nilIfEmpty= true,
split = true,
},
["..."] = {
name = "cells",
placeholder = "cell",
required = true,
type = "content",
desc = "Cell values. Type <code>|-</code> to separate rows, as shown in the examples below.",
trim = true,
}
}
},
["Data Table Copy"] = {
purpose = "Creates a copy of a {{Template|Data Table}} located on another page.",
format = "block",
paramOrder = {"fromPage", "storedAs", "columns", "excludeColumns", "rowsWith", "rowsExcluding", "responsiveModeEnabled"},
params = {
fromPage = {
required = true,
type = "wiki-page-name",
desc = "The name of the wiki page containing the data table to be copied.",
trim = true,
nilIfEmpty = true,
},
storedAs = {
required = true,
type = "string",
desc = "The internal name of the table to be copied, as specified by the <code>storeAs</code> parameter of [[Template:Data Table]].",
trim = true,
nilIfEmpty = true,
},
columns = {
type = "string",
desc = "A comma-separated list of columns to copy. If absent, all columns are copied.",
trim = true,
nilIfEmpty = true,
split = true,
},
excludeColumns = {
type = "string",
desc = "A comma-separated list of columns ''not'' to copy. If absent, all columns are copied. Ignored if <code>columns</code> is present.",
trim = true,
nilIfEmpty = true,
split = true,
},
rowsWith = {
type = "string",
desc = "If specified, only rows containing the given string will be copied. If absent, all rows are copied.",
trim = true,
nilIfEmpty = true,
},
rowsExcluding = {
type = "string",
desc = "If specified, only rows which do ''not'' contain the given string will be copied.",
trim = true,
nilIfEmpty = true,
},
responsiveModeEnabled = responsiveModeEnabledDoc,
},
},
}
return p