Module:SMWTable
This helper module is used when authoring other modules that render tables from SMW queries.
local SMWTable = require("Module:SMWTable")
Usage
To create a table, first create a new SMWTable instance:
local myTable = return SMWTable.new({
selection = "Gunbreaker's Arm",
queryOptions = {
sort = "Has level requirement,Has item level,Has canonical name",
limit = 999,
},
attributes = {
class = "table sortable",
},
})
Next, call the column method to add columns to the table, providing a definition for each column via helper functions:
myTable
:column(SMWTable.nameAndIconColumn({label = "Item"}))
:column(SMWTable.textColumn({property = "Has level requirement", label = "Level"}))
:column(SMWTable.textColumn({property = "Has item level", label = "Item level"}))
:column(SMWTable.classJobRequirementColumn())
:column(SMWTable.weaponDamageColumn())
:column(SMWTable.materiaSlotsColumn())
:column(SMWTable.attributeBonusesColumn())
Or by providing a column definition directly:
myTable:column({
label = "My column",
printouts = {
["Has some property"] = "",
},
render = function(row)
return "Value is " .. row["Has some property"]
end
})
Finally, render the table by returning it from your module's entrypoint function:
local p = {}
function p.main()
return myTable
end
return p
Class: SMWTable
Constructor
local mySMWTable = SMWTable.new({
selection: string,
queryOptions?: table,
attributes?: table,
caption?: string,
})
Creates a new table builder instance. Takes a single argument with named options. Returns a single value, the new SMWTable instance.
- selection
- The query selection string, passed through directly to
{{#ask:}}. - queryOptions
- Optional. Additional named parameters to be passed through to
{{#ask:}}, particularly useful for setting sort/order. - attributes
- Optional. Attributes attached to the table when it is rendered. Most commonly used to set
classand/orstyleto customize the table's appearance, make it sortable, etc. - Default:
{ class = "table" } - caption
- Optional. Wikitext for the table caption, if provided.
Method: column
mySMWTable:column({
label: string,
printouts: table,
render: function,
})
Adds a column to the table. Columns are rendered in the order they are added to the builder, from left to right. Takes a single column definition table which defines what data the column will display and how it will be displayed. Column definitions must have all of the following keys:
- label
- The header text displayed for this column.
- printouts
- A table describing the SMW properties that this column will display. Keys of this table are SMW property names, and the value for each key is SMW printout customization data for that property, or otherwise an empty string.
- render
- A function which takes a single argument, a single row of SMW data as a table, and renders the value of this column for the given row.
The printouts and render keys work together to define the properties necessary to render the column, and then render each row individually based on the requested data. For example, a simple column definition which displays an item's canonical name wrapped in quotation marks might look like:
mySMWTable:column({
label = "Canonical name",
printouts = {
["Has canonical name"] = "",
},
render = function(row)
return '"' .. row["Has canonical name"] .. '"'
end
})
This column definition requests one property as part of the query: Has canonical name. No special formatting is required, so the corresponding value in the printouts table is simply the empty string. Then, to render the value for each row, the render function concatenates quotation marks around the current row's property value.
More complicated columns may require multiple properties, chained properties, and/or printout formatting directives in order to achieve their function. For example, consider a table of crafting recipes with a column that displays the recipe's ingredients:
local myTable = SMWTable.new({
selection = "[[Has context::Recipe]]",
queryOptions = {limit = 1},
})
myTable:column({
label = "Ingredients",
printouts = {
-- "Has ingredient" is a Record property, so we can query its subproperties
-- "Has ingredient name" uses # to prevent automatic linking of the item
["Has ingredient.Has ingredient name"] = "#",
["Has ingredient.Has ingredient quantity"] = "",
},
render = function(row)
-- "Has ingredient" can also have more than one value, for recipes with more than one ingredient
-- Ensure the values we're working with are arrays so we can handle multiple ingredients
local names = row["Has ingredient name"]
local quantities = row["Has ingredient quantity"]
if type(names) == "string" then
names = {names}
quantities = {quantities}
end
-- Assemble the column value
local out = ""
for i, name in ipairs(names) do -- Loop over all ingredient names
if out ~= "" then
out = out .. "<br>" -- Separate ingredients with a line break
end
-- Display the ingredient name and the corresponding quantity
out = out .. name .. " x" .. quantities[i]
end
end
})
This table would render the crafting recipe for Raisins like so:
| Ingredients |
|---|
| Lowland Grapes x1 Fire Shard x1 |
Method: __tostring
tostring(mySMWTable)
Renders the table to wikitext. This is a metamethod that is invoked when the SMWTable instance is cast to a string (via tostring(), the .. operator, ...)
Static method: textColumn
mySMWTable:column(SMWTable.textColumn({
property: string,
label?: string,
default?: string,
valuesep?: string,
}))
Returns a column definition that displays the value of a string property.
- property
- Property name, e.g. "Has canonical name".
- label
- Optional. Heading text for the column.
- Default: Property name.
- default
- Optional. Default value to render when a row does not have a value for the property.
- valuesep
- Optional. Separator used when a row has multiple values for the property.
- Default:
", "
Static method: nameAndIconColumn
Static method: classJobRequirementColumn
Static method: weaponDamageColumn
Static method: materiaSlotsColumn
TODO
local Icon = require("Module:Icon")
local yesno = require("Module:Yesno")
---@class SMWTableColumn A column that displays property values of Semantic Mediawiki entities.
---@field label string Header label for the column
---@field printouts {[string]: string} Map of property names to printout options used for the property; pagename property is keyed by empty string
---@field render fun(entity: table): string Function that renders the column's value for each queried entity
---@class SMWTable A table of Semantic Mediawiki entities, composed of multiple columns.
---@field selection string SMW query selection string
---@field queryOptions table Additional SMW query options for e.g. sorting etc
---@field attributes table Table attribute customization - default is {class = "wikitable"}
---@field caption string | nil Caption content, if any
---@field groupBy string | nil Property name to group results by
---@field groupEvery integer | nil If groupBy is an integer property, groupEvery allows you to generate groups for a range of values (e.g. every 10) instead of every individual value
---@field groupHeader string
---@field groupDefault string
---@field printouts {[string]: string} Map of property names to printout options used for the property; pagename property is keyed by empty string
---@field columnLabels string[] Array of column header labels
---@field columnRenderers (fun(entity: table): string)[] Array of column render functions in the order they will appear
local SMWTable = {}
SMWTable.__index = SMWTable
---@param args {
--- selection: string,
--- queryOptions?: table,
--- attributes?: table,
--- caption?: string,
--- group?: {
--- property: string,
--- range?: number,
--- headerFormat?: string,
--- defaultContent?: string,
--- },
--- groupEvery?: integer,
---}
---@return SMWTable
function SMWTable.new(args)
---@type SMWTable
local instance = {
-- from constructor args
selection = args.selection,
queryOptions = args.queryOptions or {},
attributes = args.attributes or {class = "wikitable"},
caption = args.caption,
groupBy = args.group and args.group.property,
groupEvery = args.group and args.group.range,
groupHeader = args.group and args.group.headerFormat or "Group: %s",
groupDefault = args.group and args.group.defaultContent or "No results",
-- populated by :column()
printouts = {},
columnLabels = {},
columnRenderers = {},
}
-- Option validation
-- Always sort by the thing we're grouping by, to avoid sorting results in Lua
if instance.groupBy then
if instance.queryOptions.sort then
instance.queryOptions.sort = instance.groupBy .. "," .. instance.queryOptions.sort
else
instance.queryOptions.sort = instance.groupBy
end
end
setmetatable(instance, SMWTable)
return instance
end
-- Methods
---@param column SMWTableColumn Column definition
---@return self
function SMWTable:column(column)
-- apply printouts required for column
for propertyName, formatString in pairs(column.printouts) do
local existingFormat = self.printouts[propertyName]
if existingFormat then
if existingFormat ~= formatString then
error("Two columns requested different formats for property " .. propertyName .. ": \"" .. existingFormat .. "\", \"" .. formatString .. "\"")
end
else
self.printouts[propertyName] = formatString
end
end
table.insert(self.columnLabels, column.label)
table.insert(self.columnRenderers, column.render)
return self
end
---@param self SMWTable
---@return string
function SMWTable:__tostring()
local query = {self.selection}
for propertyName, formatString in pairs(self.printouts) do
table.insert(query, "?" .. propertyName .. formatString)
end
for key, value in pairs(self.queryOptions) do
query[key] = value
end
-- Execute query
local results = mw.smw.ask(query)
if not results or #results == 0 then
return "No results"
end
-- Render table
local function createTable()
local t = mw.html.create("table"):attr(self.attributes)
if self.caption then
t:node(mw.html.create("caption"):wikitext(self.caption))
end
local tr = mw.html.create("tr")
t:node(tr)
for _, label in ipairs(self.columnLabels) do
tr:node(mw.html.create("th"):wikitext(label))
end
return t
end
local groupBy = self.groupBy
local groupEvery = self.groupEvery
local currentGroup = nil
-- render rows
local out = ""
local t
for _, row in ipairs(results) do
-- Check if we've moved to a new group
if groupBy then
if groupEvery and (not currentGroup or row[groupBy] > currentGroup) then
if not currentGroup then
currentGroup = groupEvery
else
currentGroup = currentGroup + groupEvery
end
-- finalize and dispose the current table
out = out .. tostring(t or "")
t = nil
-- add a heading for the next group
out = out .. "\n" .. self.groupHeader:format(currentGroup - 9 .. "-" .. currentGroup) .. "\n"
-- add more headings if necessary to get up to the new value
while row[groupBy] > currentGroup do
currentGroup = currentGroup + groupEvery
out = out
.. self.groupDefault
.. "\n" .. self.groupHeader:format(currentGroup - 9 .. "-" .. currentGroup) .. "\n"
end
elseif not groupEvery and row[groupBy] ~= currentGroup then
currentGroup = row[groupBy]
-- finalize and dispose the current table
out = out .. tostring(t or "")
t = nil
-- add a heading for the next group
out = out .. "\n" .. self.groupHeader:format(row[groupBy]) .. "\n"
end
end
-- render values for this row
if not t then
t = createTable()
end
local tr = mw.html.create("tr")
t:node(tr)
for _, render in ipairs(self.columnRenderers) do
local renderedValue = tostring(render(row))
local td = mw.html.create("td")
:wikitext(renderedValue)
tr:node(td)
end
end
-- finalize the last table
out = out .. tostring(t or "")
return out
end
-- Static methods - column helpers
---@param options string | {property: string, label?: string, default?: string, valuesep?: string}
---@return SMWTableColumn
function SMWTable.textColumn(options)
if type(options) == "string" then
options = {property = options}
end
return {
label = options.label or options.property,
printouts = {
[options.property] = "",
},
render = function (item)
local value = item[options.property]
if not value then
return options.default or ""
end
if type(value) == "table" then
return table.concat(value, options.valuesep or ", ")
end
return tostring(item[options.property])
end
}
end
---@param options {name?: string, iconSize?: ("small" | "mid" | "big"), allowWrap?: boolean?}
---@return SMWTableColumn
function SMWTable.nameAndIconColumn(options)
options = options or {}
return {
label = "Name",
printouts = {
[""] = "=Pagename#",
["Has context"] = "",
["Has game icon"] = "#",
["Has canonical name"] = "",
["Has dye channels"] = "",
["Has game icon frame type"] = "",
},
render = function(item)
return Icon.gameIconWithLabel({
type = item['Has context'],
frameType = item['Has game icon frame type'],
icon = item['Has game icon'],
link = item['Pagename'],
size = options.iconSize or "big",
dyeCount = item['Has dye channels'],
text = item['Has canonical name'] or item['Pagename'],
allowWrap = options.allowWrap or false
})
end
}
end
local classJobAbbreviations = {
-- DoH/DoL
alchemist = "ALC",
armorer = "ARM",
blacksmith = "BSM",
botanist = "BTN",
carpenter = "CRP",
culinarian = "CUL",
fisher = "FSH",
goldsmith = "GSM",
leatherworker = "LTW",
miner = "MIN",
weaver = "WVR",
-- Combat classes
marauder = "MRD",
gladiator = "GLA",
pugilist = "PGL",
lancer = "LNC",
archer = "ARC",
conjurer = "CNJ",
thaumaturge = "THM",
arcanist = "ACN",
rogue = "ROG",
-- Combat jobs
["dark knight"] = "DRK",
["red mage"] = "RDM",
["black mage"] = "BLM",
["white mage"] = "WHM",
["blue mage"] = "BLM",
astrologian = "AST",
bard = "BRD",
dragoon = "DRG",
monk = "MNK",
machinist = "MCH",
ninja = "NIN",
samurai = "SAM",
scholar = "SCH",
summoner = "SMN",
paladin = "PLD",
warrior = "WAR",
gunbreaker = "GNB",
dancer = "DNC",
reaper = "RPR",
sage = "SGE",
viper = "VPR",
pictomancer = "PCT",
}
function SMWTable.classJobRequirementColumn()
return {
label = "Requirement",
printouts = {
["Is for class"] = "#",
["Is for job"] = "#",
},
render = function(item)
local out = ""
local classes = item["Is for class"] or {}
if type(classes) == "string" then
classes = {classes}
end
local jobs = item["Is for job"] or {}
if type(jobs) == "string" then
jobs = {jobs}
end
for i, className in ipairs(classes) do
if i ~= 1 then
out = out .. " "
end
out = out .. "[[" .. className .. "|" .. classJobAbbreviations[className:lower()] .. "]]"
end
out = out .. " "
for i, jobName in ipairs(jobs) do
if i ~= 1 then
out = out .. " "
end
out = out .. "[[" .. jobName .. "|" .. classJobAbbreviations[jobName:lower()] .. "]]"
end
return out
end
}
end
function SMWTable.weaponDamageColumn()
return {
label = "Damage (Type)",
printouts = {
["Is for job"] = "#",
["Has weapon physical damage"] = "",
["Has weapon magic damage"] = "",
["Has hq weapon physical damage"] = "",
["Has hq weapon magic damage"] = "",
},
render = function(item)
-- only render damage if this weapon is for a job
local job = item["Is for job"]
if not job or job == "" then return "" end
job = job:lower()
local damageType = "magic"
if job == "warrior"
or job == "paladin"
or job == "monk"
or job == "dragoon"
or job == "bard"
or job == "ninja"
or job == "dark knight"
or job == "machinist"
or job == "samurai"
or job == "gunbreaker"
or job == "dancer"
or job == "reaper"
or job == "viper"
then
damageType = "physical"
end
local out = tostring(item["Has weapon " .. damageType .. " damage"])
local hqDamage = item["Has hq weapon " .. damageType .. " damage"]
if hqDamage and hqDamage ~= "" then
out = out .. " <sup style=\"font-size: 0.7em; font-weight: bold;\">" .. mw.getCurrentFrame():expandTemplate({title = "HQ"}) .. item["Has hq weapon physical damage"] .. "</sup>"
end
if damageType == "physical" then
out = out .. "[[File:Physical Damage.png|24px|link=Damage Types#Physical]]"
else
out = out .. "[[File:Magical Damage.png|24px|link=Damage Types#Magical]]"
end
return out
end
}
end
function SMWTable.materiaSlotsColumn()
return {
label = "Materia slots",
printouts = {
["Has materia slots"] = "",
["Has advanced melding"] = "",
},
render = function(item)
local out = item["Has materia slots"]
if yesno(item["Has advanced melding"]) then
out = out .. " <span style=\"color:red\" title=\"This item can undergo advanced melding\">(5)</span>"
end
return out
end
}
end
local attributes = {
"Strength",
"Dexterity",
"Intelligence",
"Mind",
"Vitality",
"Critical Hit",
"Determination",
"Direct Hit Rate",
"Skill Speed",
"Spell Speed",
"Tenacity",
"Piety",
}
local attributePrintouts = {}
for _, v in ipairs(attributes) do
attributePrintouts["Has " .. v:lower() .. " bonus"] = ""
attributePrintouts["Has hq " .. v:lower() .. " bonus"] = ""
end
attributePrintouts["Has customizable substats"] = ""
attributePrintouts["Has random substats"] = ""
function SMWTable.attributeBonusesColumn()
return {
label = "Stats and Attributes",
printouts = attributePrintouts,
render = function(item)
local out = ""
for _, attributeName in ipairs(attributes) do
local bonus = item["Has " .. attributeName:lower() .. " bonus"]
if bonus and bonus ~= "" then
out = out .. "[[" .. attributeName .. "]] +" .. bonus
local hqBonus = item["Has hq " .. attributeName:lower() .. " bonus"]
if hqBonus and hqBonus ~= "" then
out = out .. "<sup style=\"font-size: 0.7em; font-weight: bold;\">" .. mw.getCurrentFrame():expandTemplate({title = "HQ"}) .. item["Has hq " .. attributeName:lower() .. " bonus"] .. "</sup>"
end
out = out .. " "
end
end
if yesno(item["Has customizable substats"]) then
out = out .. "[[File:Item settings icon.png|24px|link=]] ''Customizable [[Attributes#Secondary_Attributes|secondary attributes]]''"
elseif yesno(item["Has random substats"]) then
out = out .. "''Random [[Attributes#Secondary_Attributes|secondary attributes]]''"
end
return out
end
}
end
return SMWTable