July 25, 2020

👭 Knight Challenge #11 👬

Want to try your hand at these challenges? There's a couple of things you can do!
From writing, to research, to images, find your preferred way to contribute with our eleventh theme: Couples!

Latest Announcements

Module:UtilsLayout/Table

From Zelda Wiki, the Zelda encyclopedia
Jump to: navigation, search

table

table(data)

Parameters
  • data
    A Lua table representing the desired wikitable.
    [sortable]
    If true renders a table that can be sorted by any of its columns.
    [hideEmptyColumns]
    If true, columns that are completely empty (except for header and footer rows) are omitted.
    [hideEmptyRows]
    If true, rows that are completely empty (except for header cells) are omitted.
    [styles]
    Key-value pairs of CSS properties to apply to the <table> element.
    [headers]
    An array of strings to serve as a header row.
    rows
    [header]
    If true renders all cells in row as <th> elements.
    [footer]
    If true renders all cells in row as <th> elements.
    [styles]
    Key-value pairs of CSS properties to apply to each cell in the row.
    [cells]
    [header]
    If true, renders the cell as a <th> element.
    [colspan]
    [rowspan]
    [styles]
    Key-value pairs of CSS properties to apply to the cell. Overrides any conflicting row styles.
    content
    stringrows
    Wikitext content for the cell.
    Subvidisions for the cell. Think of it as a table within a table.
Returns
Examples
InputResult
Basic table with header and footer
table({
  rows = {
    {
      header = true,
      cells = {"column1", "column2", "column3"},
    },
    {
      cells = {"cell1", "cell2", "cell3"},
    },
    {
      cells = {"cell4", "cell5", "cell6"},
    },
    {
      footer = true,
      cells = {"foot1", "foot2", "foot2"},
    },
  },
})
column1column2column3
cell1cell2cell3
cell4cell5cell6
foot1foot2foot2
Shorthand syntax
table({
  headers = {"column1", "column2", "column3"},
  rows = {
    {"cell1", "cell2", "cell3"},
  },
})
column1column2column3
cell1cell2cell3
Works with pipe characters
table({
  rows = {
    {"cell | 1", "cell |} 2", "cell {| 3"},
  },
})
cell | 1cell |} 2cell {| 3
Cell spanning multiple columns
table({
  headers = {"col1", "col2", "col3", "col4", "col5"},
  rows = {
    {
      {
        colspan = 2,
        content = "spans 2 columns",
      },
      {
        colspan = 3,
        content = "spans 3 columns",
      },
    },
    {
      {
        colspan = -1,
        content = "spans all columns",
      },
    },
  },
})
col1col2col3col4col5
spans 2 columnsspans 3 columns
spans all columns
Option to hide columns when they're completely empty except for headers/footers
table({
  hideEmptyRows = true,
  hideEmptyColumns = true,
  rows = {
    {"row1", "", "row1"},
    {"row2", "", "row2"},
  },
  headers = {"not empty", "empty", "not empty"},
})
not emptynot empty
row1row1
row2row2
Option to hide rows when they're completely empty except for headers
table({
  hideEmptyRows = true,
  rows = {
    {
      {
        header = true,
        content = "Header1",
      },
      "not empty",
    },
    {
      {
        header = true,
        content = "Header2",
      },
      "",
    },
    {},
  },
})
Header1not empty
Table styles are applied at the table level. Cell styles can be specified individually, or once for the entire row.
table({
  rows = {
    {"centered", "centered"},
    {
      cells = {
        { content = "left-aligned" },
        {
          content = "right-aligned",
          styles = { ["text-align"] = "right" },
        },
      },
      styles = { ["text-align"] = "left" },
    },
  },
  styles = {
    ["text-align"] = "center",
    width = "20em",
  },
})
centeredcentered
left-alignedright-aligned
Individual cells subdivided into multiple columns and rows
table({
  rows = {
    {
      header = true,
      cells = {"Column1", "Column2", "Column3"},
    },
    {
      "A",
      {
        {"B1"},
        {"B2"},
      },
      "C",
    },
    {
      {
        {"D1", "D2"},
      },
      {
        {"E1"},
        {
          {
            content = "E2",
            header = true,
          },
          "E3",
        },
      },
      {
        {"F1"},
        {"F2"},
        {"F3"},
      },
    },
    {
      {
        {"G1", "G2", "G3"},
      },
      "H",
      {
        {},
      },
    },
  },
  styles = { ["text-align"] = "center" },
})
Column1Column2Column3
AB1C
B2
D1D2E1F1
E2E3F2
F3
G1G2G3H

tabbedTable

tabbedTable(data)

Parameters
Returns
  • A series of wikitables displayed using tabs. Useful for representing data with three dimensions, or data with too many columns.
Examples
InputResult
Tabbed table with shared headers and footers
tabbedTable({
  headerRows = {
    {"col1", "col2", "col3"},
  },
  tabs = {
    {
      label = "tab1",
      rows = {
        {"cell1", "cell2", "cell3"},
        {"cell4", "cell5", "cell6"},
      },
    },
    {
      label = "tab2",
      rows = {
        {"cell7", "cell8", "cell9"},
        {"cell10", "cell11", "cell12"},
      },
    },
  },
  footerRows = {
    {"foo12", "foot2", "foot3"},
  },
})
tab1tab2
col1col2col3
cell1cell2cell3
cell4cell5cell6
foo12foot2foot3
col1col2col3
cell7cell8cell9
cell10cell11cell12
foo12foot2foot3

local p = {}
local h = {}

local utilsFunction = require("Module:UtilsFunction")
local utilsLayout = require("Module:UtilsLayout/Tabs")
local utilsNumber = require("Module:UtilsNumber")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")

function p.table(data)
	data = h.resolveShorthand(data)
	if data.hideEmptyColumns then
		data = h.hideEmptyColumns(data)
	end
	if data.hideEmptyRows then
		data = h.hideEmptyRows(data)
	end
	data = h.splitCells(data)
	
	return h.createTable(data)
end
		
function p.tabbedTable(data)
	local tabs = {}
	local headerRows = data.headerRows or {}
	local footerRows = data.footerRows or {}
	for i, headerRow in ipairs(headerRows) do
		headerRow.header = true
	end
	for i, footerRow in ipairs(footerRows) do
		footerRow.footer = true
	end
	for i, tabData in ipairs(data.tabs) do
		local tabRows = utilsTable.concat(headerRows, tabData.rows, footerRows)
		table.insert(tabs, {
			label = tabData.label,
			content = p.table({ rows = tabRows })
		})
	end
	return utilsLayout.tabs(tabs)
end

function h.createTable(data)
	local html = mw.html.create("table"):addClass("wikitable")
	for _, row in ipairs(data.rows) do
		html:node(h.createRow(row))
	end
	
	local margins
	if data.align == "center" then
		margins = {
			margin = "0 auto"
		}
	elseif data.align == "right" then
		margins = {
			["margin-left"] = "auto"
		}
	end
	html:css(margins or {})
	html:css(data.styles or {})
	html:addClass(data.sortable and "sortable" or nil)
	
	return tostring(html)
end
function h.createRow(row)
	local html = mw.html.create("tr")
	for _, cell in ipairs(row.cells) do 
		local cellTag = (row.header or row.footer or cell.header or cell.footer) and "th" or "td"
		local colspan = cell.colspan
		local rowspan = cell.rowspan
		if colspan and colspan < 0 then
			colspan = 1000 
		end
		if rowspan and rowspan < 0 then
			rowspan = 1000
		end
		html:tag(cellTag)
			:attr("colspan", colspan)
			:attr("rowspan", rowspan)
			:css(cell.styles or {})
			:wikitext(mw.getCurrentFrame():preprocess(cell.content))
			:done()
	end
	return tostring(html)
end

function h.resolveShorthand(data)
	data = mw.clone(data)
	if data.headers then
		table.insert(data.rows, 1, {
			header = true,
			cells = data.headers
		})
	end
	for i, row in ipairs(data.rows) do
		row.cells = row.cells or utilsTable.ivalues(row)
		for j, cell in ipairs(row.cells) do
			local cell = h.resolveShorthandCell(cell)
			data.rows[i].cells[j] = cell
			if cell then
				cell.styles = utilsTable.merge(row.styles or {}, cell.styles or {})
			end
		end
		if utilsTable.isEmpty(data.rows[i]) then
			data.rows[i] = nil
		end
	end
	return data
end
function h.resolveShorthandCell(cell)
	if type(cell) == "string" or type(cell) == "number" or type(cell) == "boolean" then
		return {
			content = cell
		}
	end
	if type(cell) == "table" and utilsTable.isEmpty(cell) then
		return nil
	end
	if type(cell) == "table" and cell.content and not utilsTable.isArray(cell.content) then
		return cell
	end
	if not cell.content then
		cell = {
			content = utilsTable.ivalues(cell)
		}
	end
	if utilsTable.isArray(cell.content) then
		for i, subrow in ipairs(cell.content) do
			table.remove(cell, i)
			cell.content[i] = utilsTable.map(subrow, h.resolveShorthandCell)
		end
	end
	return cell
end

function h.hideEmptyRows(data)
	for i, row in ipairs(data.rows) do
		local isNonEmptyCell = function(cell) 
			return not cell.header and not h.isCellEmpty(cell)
		end
		local nonEmptyCells = utilsTable.filter(row.cells, isNonEmptyCell)
		if #nonEmptyCells == 0 then
			table.remove(data.rows, i)
		end
	end
	return data
end

function h.hideEmptyColumns(data)
	local totalColumns = h.countTotalColumns(data.rows)
	local emptyCellsPerColumn = {}
	for i, row in ipairs(data.rows) do
		for j, cell in ipairs(row.cells) do
			emptyCellsPerColumn[j] = emptyCellsPerColumn[j] or 0
			if (not cell.header and not cell.footer) and h.isCellEmpty(cell) then
				emptyCellsPerColumn[j] = emptyCellsPerColumn[j] + 1
			end
		end
		for i in utilsFunction.range(#row.cells + 1, totalColumns) do
			emptyCellsPerColumn[i] = emptyCellsPerColumn[i] + 1
		end
	end
	local data = mw.clone(data)
	local headerRows = utilsTable.filter(data.rows, "header")
	local footerRows = utilsTable.filter(data.rows, "footer")
	local totalRows = #data.rows - (math.max(#headerRows, #footerRows))
	for i, row in ipairs(data.rows) do
		for j, cell in ipairs(row.cells) do
			if emptyCellsPerColumn[j] == totalRows then
				row.cells[j] = nil
			end
		end
		row.cells = utilsTable.compact(row.cells)
	end
	return data
end
-- Cell is empty if:
-- its content is nil or the empty string
-- it has subdivisons where all the rows are empty
function h.isCellEmpty(cell)
	if type(cell.content) == "string" and utilsString.isBlank(cell.content) then
		return true
	end
	if type(cell.content) == "table" then
		for _, subRow in ipairs(cell.content) do
			for _, subCell in ipairs(subRow) do
				if not utilsString.isBlank(cell.subCell) then
					return true
				end
			end
		end
	end
	return false
end
function h.countTotalColumns(rows)
	local maxColumns = 0
	for _, row in ipairs(rows) do
		maxColumns = math.max(maxColumns, #row.cells)
	end
	return maxColumns
end

function h.splitCells(data)
	data = h.normalize(data)
	data = h.splitRows(data)
	data = h.applyColspans(data)
	data = h.flattenCellGroups(data)
	return data
end

function h.normalize(data)
	for i, row in ipairs(data.rows) do
		for j, cell in ipairs(row.cells) do
			data.rows[i].cells[j] = h.normalizeCell(cell)
		end
	end
	return data
end
function h.normalizeCell(cell)
	if utilsTable.isArray(cell.content) then
		return cell.content
	end
	return {{cell}}
end

function h.splitRows(data)
	data.rows = utilsTable.flatMap(data.rows, h.splitRow)
	return data
end
function h.splitRow(row)
	local splitRows = utilsTable.zip(row.cells, {})
	local cellSubrows = utilsTable.zip(splitRows)
	for i, subrows in ipairs(cellSubrows) do
		local isNotEmpty = utilsFunction.negate(utilsTable.isEmpty)
		local lastSubrow, lastSubrowIndex = utilsTable.findLast(subrows, isNotEmpty)
		if lastSubrow then
			local rowspan = #subrows - lastSubrowIndex + 1
			h.applyRowspan(lastSubrow, rowspan)
		end
	end
	local rows = {}
	for i, splitRow in ipairs(splitRows) do
		rows[i] = {
			header = row.header,
			footer = row.footer,
			row = row.styles,
			cells = splitRow,
		}
	end
	return rows
end
function h.applyRowspan(cellGroup, rowspan)
	if rowspan <= 1 or not cellGroup then
		return
	end
	for i, cell in ipairs(cellGroup) do
		if cell.rowspan and cell.rowspan > 1 then
			cell.rowspan = rowspan + cell.rowspan
		else
			cell.rowspan = rowspan
		end
	end
end

function h.applyColspans(data)
	local colspansPerColumn = h.getColspansForEachColumn(data)
	for i, row in ipairs(data.rows) do
		for j, cellGroup in ipairs(row.cells) do
			h.applyColspan(cellGroup, colspansPerColumn[j])
		end
	end
	return data
end
function h.getColspansForEachColumn(data)
	return utilsFunction.pipe(data.rows) { 
		utilsTable._map("cells"),
		utilsTable._map(
			utilsTable._map(utilsTable.size)
		),
		utilsTable.zip,
		utilsTable._map(utilsTable._padNils(utilsNumber.MIN)),
		utilsTable._map(utilsTable.max),
	}
end
function h.applyColspan(cellGroup, colspan)
	if #cellGroup > 0 and #cellGroup < colspan then
		cellGroup[#cellGroup].colspan = colspan - #cellGroup + 1
	end
end

function h.flattenCellGroups(data)
	for i, row in ipairs(data.rows) do
		row.cells = utilsTable.flatten(row.cells)
	end
	return data
end

p.Schemas = {
	table = {
		data = {
			definitions = {
				rowOptions = {
					type = "record",
					properties = {
						{
							name = "header",
							type = "boolean",
							desc = "If <code>true</code> renders all cells in row as <code><nowiki><th></nowiki></code> elements.",
						},
						{
							name = "footer",
							type = "boolean",
							desc = "If <code>true</code> renders all cells in row as <code><nowiki><th></nowiki></code> elements.",
						},
						{
							name = "styles",
							type = "map",
							keys = { type = "string" },
							values = { type = "string" },
							desc = "Key-value pairs of CSS properties to apply to each cell in the row.",
						},
					},
				},
				rows = {
					type = "array",
					items = { _ref = "#/definitions/row" },
				},
				row = {
					allOf = {
						{ _ref = "#/definitions/rowOptions" },
						{
							type = "record",
							properties = {
								{
									name = "cells",
									type = "array",
									items = { _ref = "#/definitions/cell" }
								},
							},
						}
					}
				},
				cells = {
					type = "record",
					properties = {
						{
							name = "cells",
							type = "array",
							items = { _ref = "#/definitions/cell" }
						},
					},
				},
				cell = {
					oneOf = {
						{
							type = "record",
							properties = {
								{
									name = "header",
									type = "boolean",
									desc = "If <code>true</code>, renders the cell as a <code><nowiki><th></nowiki></code> element.",
								},
								{
									name = "colspan",
									type = "number",
								},
								{
									name = "rowspan",
									type = "number",
								},
								{
									name = "styles",
									type = "map",
									keys = { type = "string" },
									values = { type = "string" },
									desc = "Key-value pairs of CSS properties to apply to the cell. Overrides any conflicting row styles.",
								},
								{
									name = "content",
									required = true,
									oneOf = {
										{
											type = "string",
											desc = "Wikitext content for the cell.",
										},
										{
											type = "array",
											_ref = "#/definitions/rows",
											desc = "Subvidisions for the cell. Think of it as a table within a table."
										}
									}
								}
							},
						}
					}
				},
			},
			
			type = "record",
			required = true,
			desc = "A Lua table representing the desired wikitable.",
			properties = {
				{
					name = "sortable",
					type = "boolean",
					desc = "If <code>true</code> renders a table that can be sorted by any of its columns."
				},
				{
					name = "hideEmptyColumns",
					type = "boolean",
					desc = "If <code>true</code>, columns that are completely empty (except for header and footer rows) are omitted.",
				},
				{
					name = "hideEmptyRows",
					type = "boolean",
					desc = "If <code>true</code>, rows that are completely empty (except for header cells) are omitted.",
				},
				{
					name = "styles",
					type = "map",
					keys = { type = "string" },
					values = { type = "string" },
					desc = "Key-value pairs of CSS properties to apply to the <code><nowiki><table></nowiki></code> element.",
				},
				{
					name = "headers",
					type = "array",
					items = { type = "string" },
					desc = "An array of strings to serve as a header row.",
				},
				{
					name = "rows",
					required = true,
					type = "array",
					items = {
						allOf = {
							{ _ref = "#/definitions/rowOptions" },
							{ _ref = "#/definitions/cells" },
						}
					},
				},
			},
		}
	},
	tabbedTable = {
		data = {
			type = "record",
			required = true,
			properties = {
				{
					name = "headerRows",
					type = "any",
					typeLabel = "rows",
					desc = "Header rows common to every tab.",
				},
				{
					name = "headerRows",
					type = "any",
					typeLabel = "rows",
					desc = "Footer rows common to every tab.",
				},
				{
					name = "tabs",
					required = true,
					allOf = {
						{
							type = "record",
							properties = {
								{
									name = "label",
									type = "string",
									required = true,
									desc = "Tab label",
								},
								{
									name = "rows",
									type = "any",
									required = true,
									typeLabel = "rows",
									desc = "Table rows for the tab. See {{Sect|table}} for format.",
								}
							},
						},
						
					}
				}
			}
		},
	},
}

p.Documentation = {
	table = {
		params = {"data"},
		returns = "Wikitext for the table using {{mediawiki|Help:Table#Other table syntax|XHTML syntax}}.",
		cases = {
			resultOnly = true,
			{
				desc = "Basic table with header and footer",
				args = {
					{
						rows = {
							{
								header = true,
								cells = { 'column1', 'column2', 'column3'},
							},
							{
								cells = {'cell1', 'cell2', 'cell3'},
							},
							{
								cells = {'cell4', 'cell5', 'cell6'},
							},
							{
								footer = true,
								cells = {'foot1', 'foot2', 'foot2'},
							},
						},
					},
				},
			},
			{
				desc = "Shorthand syntax",
				args = {
					{
						rows = {
							{'cell1', 'cell2', 'cell3'}
						},
						headers = {'column1', 'column2', 'column3'},
					},
				},
			},
			{
				desc = "Works with pipe characters",
				args = {
					{
						rows = {
							{"cell | 1", "cell |} 2", "cell {| 3"},
						},
					}
				},
			},
			{
				desc = "Cell spanning multiple columns",
				args = {
					{
						rows = {
							{ 
								{
									colspan = 2,
									content = "spans 2 columns",
								},
								{
									colspan = 3,
									content = "spans 3 columns",
								},
							},
							{
								{
									colspan = -1,
									content = "spans all columns",
								},
							},
						},
						headers = {"col1", "col2", "col3", "col4", "col5"}
					}
				},
			},
			{
				desc = "Option to hide columns when they're completely empty except for headers/footers",
				args = {
					{
						hideEmptyRows = true,
						hideEmptyColumns = true,
						rows = {
							{"row1", "", "row1"},
							{"row2", "", "row2"},
						},
						headers = {"not empty", "empty", "not empty"},
					}
				}
			},
			{
				desc = "Option to hide rows when they're completely empty except for headers",
				args = {
					{
						hideEmptyRows = true,
						rows = {
							{ { header = true, content = "Header1"}, "not empty"},
							{ { header = true, content = "Header2" }, "" },
							{ },
						}
					}
				}
			},
			{
				desc = "Table styles are applied at the table level. Cell styles can be specified individually, or once for the entire row.",
				args = {
					{
						styles = {
							["width"] = "20em",
							["text-align"] = "center",
						},
						rows = {
							{ "centered", "centered" },
							{
								styles = {
									["text-align"] = "left"
								},
								cells = {
									{ content = "left-aligned" },
									{
										content = "right-aligned",
										styles = {
											["text-align"] = "right",
										}
									},
								},
							},
						},
					},
				},
			},
			{
				desc = "Individual cells subdivided into multiple columns and rows",
				args = {
					{
						styles = { ["text-align"] = "center" },
						rows = {
							{ 
								header = true,
								cells = {"Column1", "Column2", "Column3"},
							},
							{
								"A",
								{ 
									{"B1"},
									{"B2"},
								},
								"C"
							},
							{
								{
									{"D1", "D2"}
								},
								{
									{"E1"},
									{{ content = "E2", header = true }, "E3"},
								},
								{
									{"F1"},
									{"F2"},
									{"F3"},
								}
							},
							{
								{
									{ "G1", "G2", "G3"}
								},
								"H",
								{{}},
							}
						},
					},
				},
			}
		}
	},
	tabbedTable = {
		params = {"data"},
		returns = "A series of wikitables displayed using tabs. Useful for representing data with three dimensions, or data with too many columns.",
		cases = {
			resultOnly = true,
			{
				desc = "Tabbed table with shared headers and footers",
				args = {
					{
						headerRows = {
							{"col1", "col2", "col3"}
						},
						footerRows = {
							{"foo12", "foot2", "foot3"}
						},
						tabs = {
							{
								label = "tab1",
								rows = {
									{'cell1', 'cell2', 'cell3'},
									{'cell4', 'cell5', 'cell6'},
								}
							},
							{
								label = "tab2",
								rows = {
									{'cell7', 'cell8', 'cell9'},
									{'cell10', 'cell11', 'cell12'},
								},
							},
						},
					},
				},
			},
		},
	},
}
		
return p