Module:Data Table

From Zelda Wiki, the Zelda encyclopedia
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

ParameterStatusDescription
gameoptionalSee Template:Data Table
columnsoptionalSee Template:Data Table
optionalColumnsoptionalSee 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

#InputOutput
1
parseRows({
  "-",
  "cell1",
  "cell2",
  "-",
  "cell3",
  "cell4",
  "-",
})
{
  {
    cells = {"cell1", "cell2"},
  },
  {
    cells = {"cell3", "cell4"},
  },
}

printTable

printTable(rows, args, [postFormatHook])

Parameters

Returns

  • A data table.

Examples

#InputResult
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