Module:Data Table

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 storeFn to store data in a custom table function p.Main(frame, storeFn, extensionArgsFn) local frameArgs = utilsTable.merge({}, frame:getParent.args, frame.args) local args, err = utilsArg.parse(frameArgs, p.Templates["Data Table"]) local categories = err and err.categoryText or "" args.columnNames = h.extractColumnTags(args.columns)

local rows = p.parseRows(args.cells) local storeCategories = h.storeTable(args, storeFn, rows) categories = categories..storeCategories

local dataTable = p.printTable(rows, args) local extensionArgs = extensionArgsFn and extensionArgsFn(args) or {} 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.."" 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..""

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.."" 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 h.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  is stored on page %s", args.storedAs, args.fromPage)) return err, categories.."" 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 = h.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 = h.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  does not exist in table   stored on page %s. The columns defined for this table are as follows: %s", copyHeader, args.storedAs, args.fromPage, utilsMarkup.bulletList(headerNames))) categories = categories.."" 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(" ", args.rowsWith) local excludingCriteria = args.rowsExcluding and string.format(" ", 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  on page %s", criteriaMsg, args.storedAs, args.fromPage)) return err, categories.."" 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 = h.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", 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 ", tag.name)) categories = categories.."" 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) -- 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 then return "" end local html = mw.html.create("div") :addClass("data-table data-table--mobile size-medium-down") :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 = ' [ editing the page] ' local message = string.format(" The following data table is incomplete. You can help by %s to %s. ", editPage, addDataAction) message = mw.getCurrentFrame:preprocess(message) message = message.."" local game = args.game game = game and Franchise.baseGame(game) game = game and Franchise.shortName(game) if game then message = message..string.format("", 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  for other 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 templates with fixed values for the parameters   and/or , 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", },			},		},		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.", "A string of error categories, or an empty string if there are none.", },			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  property allows columns to be omitted", args = { {							{								cells = {"Treasure Chest", "Small Key"}, },							{								cells = {"Treasure Chest", "Red Rupee"}, }						},						{							game = "PH", columns = {"Treasure Chest [Term]", "Contents [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("The template estimates the width of the widest row to be greater than %d characters.", responsiveModeThresholds.minWidth) .."\n* "..string.format("The table has less than %d rows.", 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 ." .."\n* The page is in the main namespace." .."\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  or , respectively.", trim = true, nilIfEmpty = true, } p.Templates = { ["Data Table/Store"] = { purpose = "Stores data into the DataTables Cargo table, for use by .", storesData = true, usage = "This template is transcluded by Module:Data Table.", },	["Data Table"] = { format = "block", purpose = " Displays tabular data in a way that is mobile-friendly. Minimizes the amount of boilerplate wikitext that most tables require. Automatically center-aligns columns and applies templates such as, , , etc.  The table data can stored in Cargo so that the table (or a subset of its rows) can be displayed on other relevant pages using .", boilerplate = { separateRequiredParams = false, },		paramOrder = {"game", "storeAs", "caption", "responsiveModeEnabled", "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., , ) to table cells.", },			storeAs = { type = "string", desc = " If present, the data is stored in the DataTables Cargo table under the given name. Other pages can retrieve the data by that name using . When a page has multiple data tables, each   value must be unique. ", trim = true, nilIfEmpty = true, },			caption = { type = "string", desc = "A table caption.", trim = true, nilIfEmpty = true, },			responsiveModeEnabled = responsiveModeEnabledDoc, sortable = { type = "boolean", desc = "If set to, the data table will not be sortable.", trim = true, },			stretch = { type = "boolean", desc = "If present and set to anything other than, the data table will stretch to the full width of the page.", },			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 below.", trim = true, nilIfEmpty= true, split = true, },			["..."] = {				name = "cells", placeholder = "cell", required = true, type = "content", desc = "Cell values. Type  to separate rows, as shown in the examples below.", trim = true, }		}	},	["Data Table Copy"] = { purpose = "Creates a copy of a 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  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  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