Module:Data Table

local p = {} local h = {}

local File = require("Module:File") local Franchise = require("Module:Franchise") local Sequences = require("Module:Sequences") local Term = require("Module:Term") local TermList = require("Module:Term List") 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")

-- Other modules can extend this one via extensionArgs and by using storeFn to store data in a custom table function p.Main(frame, extensionArgs, storeFn) local frameArgs = utilsTable.merge({}, frame:getParent.args, extensionArgs) local args, err = utilsArg.parse(frameArgs, p.Templates["Data Table"]) local categories = err and err.categoryText or "" local columnHeaders, columnTags = h.extractColumnTags(args.columns) local name = args.name or columnHeaders[1] local tableId = utilsString.kebabCase(name) local unsortableColumns = {} local dataRows = {} local currentRow = { cells = {} }	local columnIndex = 1 for i, cell in ipairs(args.cells or {}) do		if cell == "-" and i ~= 1 then currentRow.id = tableId and tableId.."-"..(#dataRows+1) table.insert(dataRows, currentRow) currentRow = { cells = {} }			columnIndex = 1 elseif cell ~= "-" then local cell, sortable = h.formatCell(args.game, columnTags[columnIndex], cell) cell.columnHeader = columnHeaders[columnIndex] table.insert(currentRow.cells, cell) if not sortable or args.sortable == "false" then unsortableColumns[columnIndex] = true end columnIndex = columnIndex + 1 end end currentRow.id = tableId and tableId.."-"..(#dataRows+1) if #currentRow.cells > 0 then table.insert(dataRows, currentRow) end

h.store(args, dataRows, unsortableColumns) if storeFn then storeFn(args, dataRows, columnHeaders) end local desktopTable = h.desktopTable(columnHeaders, dataRows, unsortableColumns) local mobileTable = h.mobileTable(columnHeaders, dataRows) local styles = frame:extensionTag({		name = "templatestyles",		args = { src = "Template:Data Table/Styles.css" }	}) return styles..desktopTable..mobileTable, categories end

function p.Copy(frame) local args, err = utilsArg.parse(frame:getParent.args, p.Templates["Data Table Copy"]) local errCategories = err and err.categoryText or "" if not args.fromPage or not args.name then return "", errCategories end local dataTable, categories = p.copy(args) return dataTable, categories..errCategories end

function p.copy(args, filterFn) local categories = ""

local queryResults = utilsCargo.query("DataTables", "name, rowIndex, rowId, columnIndex, columnHeader, columnSortable, cellClass, cellContent, cellSortValue, cellRaw", {		where = utilsCargo.allOf({ _pageName = args.fromPage, name = args.name, game = args.game, }),		orderBy= "game, name, rowIndex, columnIndex",		limit= 1000	}) if #queryResults == 0 then h.warn(string.format("No table  exists on page %s", args.name, args.fromPage)) return "", categories.."" end local dataRows = {} local currentRow = {} for i, result in ipairs(queryResults) do		if i ~= 1 and result.columnIndex == "1" then table.insert(dataRows, currentRow) currentRow = {} end table.insert(currentRow, result) end table.insert(dataRows, currentRow)

dataRows = utilsTable.map(dataRows, function(row)		return {			id = row[1].rowId,			cells = utilsTable.map(row, function(cell) return { class = cell.cellClass, columnHeader = cell.columnHeader, content = cell.cellContent, sortValue = cell.cellSortValue, raw = cell.cellRaw, }			end)		}	end) if args.rowsWith then dataRows = utilsTable.filter(dataRows, function(row)			local includeRow = false			for i, cell in ipairs(row.cells) do				if string.find(cell.raw, args.rowsWith) then					includeRow = true				end			end			return includeRow		end) end if filterFn then dataRows = utilsTable.filter(dataRows, filterFn) end local columns = utilsTable.uniqueBy(dataRows[1] and dataRows[1].cells or {}, function(cell)		return cell.columnHeader	end) local columnHeaders = {} local columnLookup = {} for i, column in ipairs(columns) do		columnHeaders[i] = column.columnHeader columnLookup[column.columnHeader] = column end local hasInvalidColumns = false local columnsToCopy if args.columns or args.excludeColumns then if args.columns then columnsToCopy = {} else columnsToCopy = utilsTable.invert(columnHeaders) end local validColumnsList = utilsMarkup.bulletList(columnHeaders) for i, column in ipairs(args.columns or args.excludeColumns) do			if not columnLookup[column] then hasInvalidColumns = true h.warn(string.format("Column  does not exist in table   on page %s. The columns defined for this table are as follows: %s", column, args.name, args.fromPage, validColumnsList)) categories = categories.."" elseif args.columns then columnsToCopy[column] = true elseif args.excludeColumns then columnsToCopy[column] = false end end end if columnsToCopy and not hasInvalidColumns then for i, row in ipairs(dataRows) do			local cells = {} for j, cell in ipairs(row.cells) do				if columnsToCopy[cell.columnHeader] then table.insert(cells, cell) end end row.cells = cells end columnHeaders = utilsTable.filter(columnHeaders, function(header)			return columnsToCopy[header]		end) end local unsortableColumns = {} for i, column in ipairs(columnHeaders) do		if columnLookup[column] and columnLookup[column].columnSortable ~= "1" then unsortableColumns[i] = true end end local styles = mw.getCurrentFrame:extensionTag({		name = "templatestyles",		args = { src = "Template:Data Table/Styles.css" }	}) local desktopTable = h.desktopTable(columnHeaders, dataRows, unsortableColumns) local mobileTable = h.mobileTable(columnHeaders, dataRows) return styles..desktopTable..mobileTable, categories end

function h.extractColumnTags(columns) local columnHeaders = {} local columnTags = {} for i, column in ipairs(columns) do		columnHeaders[i] = string.gsub(column, "%s*%[(%w+)%]$", function(tag)			columnTags[i] = tag			return ""		end) end return columnHeaders, columnTags end

function h.formatCell(game, tag, cellText) local frame = mw.getCurrentFrame local cell = {} local cellData = utilsString.split(cellText) sortable = tag ~= nil and #cellData == 1 if #cellData == 1 and cellData[1] == "" then cell.class = "data-table__cell data-table__cell--empty" cell.content = "" elseif #cellData == 1 and cellData[1] == "N/A" then cell.class = "data-table__cell data-table__cell--not-applicable" cell.content = utilsMarkup.tooltip("N/A", "Not Applicable") elseif tag == "Amount" or tag == "Amounts" then cell.class = "data-table__cell data-table__cell--amounts" sortable = false local amounts = utilsTable.map(cellData, function(itemAmount)			local amountStart, amountEnd, amount = string.find(itemAmount, "^(%d+)%s")			if not amount then				h.warn(string.format("Invalid entry . Amount columns must start with a number.", itemAmount))				cell.content = ""				return itemAmount			else				local item = string.sub(itemAmount, amountEnd+1)				return frame:expandTemplate({ title = "Item Amount", args = {game, amount, item} })			end		end) cell.content = (cell.content or "")..h.list(amounts) elseif tag == "Description" then cell.class = "data-table__cell data-table__cell--description" cell.content = cellText elseif tag == "Image" then cell.class = "data-table__cell data-table__cell--image" cell.sortValue = cellData[1] local term = Term.fetchTerm(cellData[1], game) or cellData[1] local image = File.icon(game, term, {			size = "64x64px",		}) local termLink = Term.link(cellData[1], game) termLink = string.gsub(termLink, " %+", " +") -- a hack to fix the + wrapping on a separate line for terms like "Air Potion +" cell.content = tostring(mw.html.create("div")			:addClass("data-table__image-term")			:tag("div")				:wikitext(image)				:done			:tag("div")				:wikitext(termLink)				:done) if #cellData > 1 then h.warnMultiple("Image", cellData) cell.content = cell.content.."" end elseif tag == "Rupees" then cell.class = "data-table__cell data-table__cell--amounts" if #cellData == 1 then cell.sortValue = cellData[1] end local amounts = utilsTable.map(cellData, function(amount)			return frame:expandTemplate({ title = "Rupee", args = {game, amount} })		end) cell.content = h.list(amounts) elseif tag == "SortValue" then cell.class = "data-table__cell data-table__cell--terms" cell.sortValue = Sequences.sortValue(game, nil, cellData[1]) cell.content = Term.link(cellData[1], game) if #cellData > 1 then h.warnMultiple("SortValue", cellData) cell.content = cell.content.."" end elseif tag == "Term" or tag == "Terms" then cell.class = "data-table__cell data-table__cell--terms" local termLinks = TermList.termList(game, cellData, { link = true }) cell.content = h.list(termLinks) else cell.class = "data-table__cell" cell.content = cellText end cell.raw = cellText return cell, sortable end function h.warnMultiple(tag, cellData) h.warn(string.format("Tag  cannot be used on cells containing multiple values:  ", tag, table.concat(cellData, ", "))) end

function h.list(items) return #items > 1 and utilsMarkup.list(items) or items[1] end

local frame = mw.getCurrentFrame function h.store(args, dataRows, unsortableColumns) for i, row in ipairs(dataRows) do		for j, cell in ipairs(row.cells) do			frame:expandTemplate({				title = "Data Table/Store",				args = {					game = args.game,					name = args.name,					rowIndex = i,					rowId = row.id,					columnIndex = j,					columnHeader = cell.columnHeader,					columnSortable = unsortableColumns[j] and "0" or "1",					cellClass = cell.class,					cellContent= cell.content,					cellRaw = cell.raw,					cellSortValue = cell.sortValue,				}			}) end end end

function h.desktopTable(headerRows, dataRows, unsortableColumns) local sortableColumns = {} for i = 1, #headerRows do		if not unsortableColumns[i] then table.insert(sortableColumns, i)		end end local wikitable = utilsLayout.table({		sortable = sortableColumns,		headers = headerRows,		rows = dataRows,	}) local html = mw.html.create("div") :addClass("data-table data-table--desktop size-large-up") :wikitext(wikitable) return tostring(html) end

function h.mobileTable(columnHeaders, dataRows) local html = mw.html.create("div") :addClass("data-table data-table--mobile size-medium-down") :tag("div") :addClass("data-table__list") for i, row in ipairs(dataRows) do		local item = html:tag("table"):addClass("wikitable data-table__list-item") for j, header in ipairs(columnHeaders) do			local cell = row.cells[j] or {} item:tag("tr") :tag("th") :addClass("data-table__row-header") :wikitext(header) :done :tag("td") :addClass(cell.class) :wikitext(cell.content) :done end end return tostring(html:allDone) end

function h.warn(msg) local utilsError = require("Module:UtilsError") utilsError.warn(msg) end

p.Templates = { ["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 is 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 = {"name", "game", "sortable", "columns", "..."}, params = { name = { type = "string", required = true, desc = "Assigns an internal name to the table so that other pages can retrieve it using .", },			game = { type = "string", required = true, enum = Franchise.enum, desc = "A game code. Used to automatically apply game-based templates (e.g., , ) to table cells.", },			sortable = { type = "boolean", desc = "If set to, the data table will not be sortable.", trim = true, },			columns = { type = "content", required = true, desc = "Comma-separated list of column headers. One 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", "name", "columns", "excludeColumns", "rowsWith"}, 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, },			name = { 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, },		},	}, }

return p