Module:PerksView: Difference between revisions

From Against the Storm Official Wiki
(Created to replace the table templates that are way too slow)
 
(hiding all sources should work now)
 
(5 intermediate revisions by the same user not shown)
Line 12: Line 12:


--region Private constants
--region Private constants
--local ARG_ID_LIST = "id"
--local ARG_NAME_LIST = "name"
--local ARG_DESCRIPTION_LIST = "description"
--local ARG_SEARCH_ALL_LIST = "search"
--local ARG_RARITY = "rarity"
--local ARG_SOURCE = "source"
--local ARG_EXCLUDE_LIST = "exclude"
--local ARG_CAPTION = "caption"
--
--local ARG_DISPLAY_OVERRIDE = "display"
--local ARG_DISPLAY_OVERRIDE_OPTION_LIST = "list"
--local ARG_DISPLAY_OVERRIDE_OPTION_INLINE = "inline"
--local ARG_LIST_TYPE = "list_type"


local DEFAULT_CAPTION = "Perks and Cornerstones"
local DEFAULT_CAPTION = "Perks and Cornerstones"
Line 32: Line 18:
local CLASS_UNSORTABLE = "unsortable"
local CLASS_UNSORTABLE = "unsortable"


local ATTR_SPAN_TWO_ROWS = { rowspan="2" }
local PERK_LINK_ICON_SIZE = "large"


local HEADER_ID = "ID"
local HEADER_ID = "ID"
Line 105: Line 91:
function PerksView.ViewParameters.isShowingPrice(self)
function PerksView.ViewParameters.isShowingPrice(self)
     return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_PRICE]
     return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_PRICE]
end
function PerksView.ViewParameters.countShownSourceColumns(self)
    local count = 0
    if self:isShowingSourceAltar() then
        count = count + 1
    end
    if self:isShowingSourceCornerstone() then
        count = count + 1
    end
    if self:isShowingSourceOrder() then
        count = count + 1
    end
    if self:isShowingSourceRelic() then
        count = count + 1
    end
    if self:isShowingSourceTrader() then
        count = count + 1
    end
    -- No matter what, one column.
    if 0 == count then
        return 1
    else
        return count
    end
end
end


Line 133: Line 146:
function PerksView.constructViewParametersFromTemplateFrame(frame)
function PerksView.constructViewParametersFromTemplateFrame(frame)
     local newInstance = {}
     local newInstance = {}
     newInstance[ARG_SKIP_SOURCES] = frame.args[ARG_SKIP_SOURCES] or ARG_SKIP_FLAG_VALUE
     newInstance[ARG_SKIP_SOURCES] = frame.args[ARG_SKIP_SOURCES] or "not by default"
     newInstance[ARG_SHOW_ID] = frame.args[ARG_SHOW_ID] or ARG_SHOW_FLAG_VALUE
     newInstance[ARG_SHOW_ID] = frame.args[ARG_SHOW_ID] or "not by default"
     newInstance[ARG_SHOW_RARITY] = frame.args[ARG_SHOW_RARITY] or ARG_SHOW_FLAG_VALUE
     newInstance[ARG_SHOW_RARITY] = frame.args[ARG_SHOW_RARITY] or ARG_SHOW_FLAG_VALUE
     newInstance[ARG_SHOW_DESCRIPTION] = frame.args[ARG_SHOW_DESCRIPTION] or ARG_SHOW_FLAG_VALUE
     newInstance[ARG_SHOW_DESCRIPTION] = frame.args[ARG_SHOW_DESCRIPTION] or ARG_SHOW_FLAG_VALUE
Line 202: Line 215:
     -- Assign the methods so they can be chained
     -- Assign the methods so they can be chained
     cell.spanTwoRows = function(self)
     cell.spanTwoRows = function(self)
         self:attr(ATTR_SPAN_TWO_ROWS)
         self:attr({ rowspan="2" })
         return self
         return self
     end
     end
Line 209: Line 222:
         return self
         return self
     end
     end
    cell.spanMultipleColumns = function(self, columnsToSpan)
        self:attr({ colspan=columnsToSpan })
        return self
    end


     return cell
     return cell
Line 247: Line 265:
         { label = HEADER_RARITY, cell = createHeaderCell(HEADER_RARITY):spanTwoRows() },
         { label = HEADER_RARITY, cell = createHeaderCell(HEADER_RARITY):spanTwoRows() },
         { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):spanTwoRows():makeUnsortable() },
         { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):spanTwoRows():makeUnsortable() },
         { label = HEADER_SOURCES, cell = createHeaderCell(HEADER_SOURCES) },
         { label = HEADER_SOURCES, cell = createHeaderCell(HEADER_SOURCES):spanMultipleColumns(viewParameters:countShownSourceColumns()) },
         { label = HEADER_PRICE, cell = createHeaderCell(HEADER_PRICE):spanTwoRows() }
         { label = HEADER_PRICE, cell = createHeaderCell(HEADER_PRICE):spanTwoRows() }
     }
     }
Line 303: Line 321:


     -- Configure the table; we'll figure out which to actually show in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
     -- Configure the table; we'll figure out which to actually show in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
     local cellsInOrder = {
     local cellsInOrderWithoutSources = {
        { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
        { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
        { label = HEADER_RARITY, cell = createDataCell(data[HEADER_RARITY] or "—") },
        { label = HEADER_DESCRIPTION, cell = createDataCell(data[HEADER_DESCRIPTION] or "—") },
        { label = HEADER_PRICE, cell = createDataCell(data[HEADER_PRICE] or "—") }
    }
    local cellsInOrderWithSources = {
         { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
         { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
         { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
         { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
Line 316: Line 341:
     }
     }


     local dataRow = mw.html.create("tr")
     -- If skipping the sources altogether overrides individual column visibility.
     dataRow:newline()
    if viewParameters:isShowingSources() then
    addAllCellsThatShouldBeShowing(dataRow, cellsInOrder, viewParameters)
 
        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithSources, viewParameters)
 
        htmlTable:node(dataRow):newline()
 
        return dataRow
 
     else
        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithoutSources, viewParameters)


    htmlTable:node(dataRow):newline()
        htmlTable:node(dataRow):newline()


    return dataRow
        return dataRow
 
    end
end
end


Line 363: Line 402:
         end
         end
     end
     end
end
local function createPerkLink(id)
    return mw.getCurrentFrame():expandTemplate{
        title = "pl",
        args = { ["id"] = id, ["iconsize"] = PERK_LINK_ICON_SIZE }
    }
end
end


Line 372: Line 419:


---startTable
---startTable
---@param caption table
---@param caption string the desired caption
---@param viewParameters table
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table
---@return table an html node, the htmlTable member variable
function PerksView.startTable(caption, viewParameters)
function PerksView.startTable(caption, viewParameters)


Line 384: Line 431:


---addRow
---addRow
---@param id table
---@param id string the ID of the perk
---@param name table
---@param _ string the name of the perk is not actually used, but asked for because the Controller doesn't need to know
---@param rarity table
---@param rarity string the rarity of the perk expressed as a string
---@param description table
---@param description string the description of the perk
---@param isSourceAltar table
---@param isSourceAltar boolean whether the perk is acquired at the altar
---@param isSourceCornerstone table
---@param isSourceCornerstone boolean whether the perk is acquired as a cornerstone
---@param isSourceOrder table
---@param isSourceOrder boolean whether the perk is acquired from orders
---@param isSourceRelic table
---@param isSourceRelic boolean whether the perk is acquired from relics
---@param isSourceTrader table
---@param isSourceTrader boolean whether the perk is acquired from traders
---@param price table
---@param price number the purchase price at a trader
---@param viewParameters table
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table
---@return table an html node, the htmlTable member variable
function PerksView.addRow(id, name, rarity, description, isSourceAltar, isSourceCornerstone, isSourceOrder, isSourceRelic, isSourceTrader, price, viewParameters)
function PerksView.addRow(id, _, rarity, description, isSourceAltar, isSourceCornerstone, isSourceOrder, isSourceRelic, isSourceTrader, price, viewParameters)


     local data = {
     local data = {
         [HEADER_ID] = validateStringData(id),
         [HEADER_ID] = validateStringData(id),
         [HEADER_NAME] = validateStringData(name),
         [HEADER_NAME] = createPerkLink(validateStringData(id)),
         [HEADER_RARITY] = validateStringData(rarity),
         [HEADER_RARITY] = validateStringData(rarity),
         [HEADER_DESCRIPTION] = validateStringData(description),
         [HEADER_DESCRIPTION] = validateStringData(description),
Line 412: Line 459:


     addDataRow(data, viewParameters)
     addDataRow(data, viewParameters)
    return htmlTable
end
---finalize
---@return table the complete html table node, fully rendered
function PerksView.finalize()


     return htmlTable
     return htmlTable

Latest revision as of 01:14, 24 June 2024

Documentation for this module may be created at Module:PerksView/doc

---
--- Serves the Perks searching template by capturing input and using it to control the display of data.
---
---@module PerksView
local PerksView = {}

--region Dependencies

--endregion



--region Private constants

local DEFAULT_CAPTION = "Perks and Cornerstones"

local CLASS_PERKS_TABLE = "wikitable sortable mw-collapsible"
local CLASS_UNSORTABLE = "unsortable"

local PERK_LINK_ICON_SIZE = "large"

local HEADER_ID = "ID"
local HEADER_NAME = "Name"
local HEADER_RARITY = "Rarity"
local HEADER_DESCRIPTION = "Description"
local HEADER_SOURCES = "Sources"
local HEADER_PRICE = "Price"

local HEADER_ABBR_ALTAR = tostring(mw.html.create("abbr"):attr({ title="Altar (Stormforged Cornerstone)" }):wikitext("Alt"))
local HEADER_ABBR_CORNERSTONE = tostring(mw.html.create("abbr"):attr({ title="Cornerstone" }):wikitext("Cor"))
local HEADER_ABBR_ORDER = tostring(mw.html.create("abbr"):attr({ title="Order"}):wikitext("Ord"))
local HEADER_ABBR_RELIC = tostring(mw.html.create("abbr"):attr({ title="Relic (Glade Event)" }):wikitext("Rel"))
local HEADER_ABBR_TRADER = tostring(mw.html.create("abbr"):attr({ title="Trader" }):wikitext("Tra"))

local RED_X = mw.getCurrentFrame():expandTemplate{ title = "c", args = { [1] = "Red", [2] = "x" } }

--endregion



--region Public sub-class

--- This subclass should never be created directly, but only via the constructFromTemplateFrame method, to ensure data integrity and minimize chance for errors.
PerksView.ViewParameters = {}

-- Indexes
local ARG_SKIP_SOURCES = "skip_sources"
local ARG_SHOW_ID = "show_id"
local ARG_SHOW_RARITY = "show_rarity"
local ARG_SHOW_DESCRIPTION = "show_description"
local ARG_SHOW_SOURCE_ALTAR = "show_source_altar"
local ARG_SHOW_SOURCE_CORNERSTONE = "show_source_cornerstone"
local ARG_SHOW_SOURCE_ORDER = "show_source_order"
local ARG_SHOW_SOURCE_RELIC = "show_source_relic"
local ARG_SHOW_SOURCE_TRADER = "show_source_trader"
local ARG_SHOW_PRICE = "show_price"
-- Flags
local ARG_SKIP_FLAG_VALUE = "skip"
local ARG_SHOW_FLAG_VALUE = "show"

function PerksView.ViewParameters.isShowingID(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_ID]
end
function PerksView.ViewParameters.isShowingName()
    return true
end
function PerksView.ViewParameters.isShowingRarity(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_RARITY]
end
function PerksView.ViewParameters.isShowingDescription(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_DESCRIPTION]
end
function PerksView.ViewParameters.isShowingSources(self)
    return ARG_SKIP_FLAG_VALUE ~= self[ARG_SKIP_SOURCES]
end
function PerksView.ViewParameters.isShowingSourceAltar(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SOURCE_ALTAR]
end
function PerksView.ViewParameters.isShowingSourceCornerstone(self)
   return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SOURCE_CORNERSTONE]
end
function PerksView.ViewParameters.isShowingSourceOrder(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SOURCE_ORDER]
end
function PerksView.ViewParameters.isShowingSourceRelic(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SOURCE_RELIC]
end
function PerksView.ViewParameters.isShowingSourceTrader(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_SOURCE_TRADER]
end
function PerksView.ViewParameters.isShowingPrice(self)
    return ARG_SHOW_FLAG_VALUE == self[ARG_SHOW_PRICE]
end

function PerksView.ViewParameters.countShownSourceColumns(self)

    local count = 0
    if self:isShowingSourceAltar() then
        count = count + 1
    end
    if self:isShowingSourceCornerstone() then
        count = count + 1
    end
    if self:isShowingSourceOrder() then
        count = count + 1
    end
    if self:isShowingSourceRelic() then
        count = count + 1
    end
    if self:isShowingSourceTrader() then
        count = count + 1
    end

    -- No matter what, one column.
    if 0 == count then
        return 1
    else
        return count
    end
end

local checkerSwitch = {
    [HEADER_ID] = PerksView.ViewParameters.isShowingID,
    [HEADER_NAME] = PerksView.ViewParameters.isShowingName,
    [HEADER_RARITY] = PerksView.ViewParameters.isShowingRarity,
    [HEADER_DESCRIPTION] = PerksView.ViewParameters.isShowingDescription,
    [HEADER_SOURCES] = PerksView.ViewParameters.isShowingSources,
    [HEADER_PRICE] = PerksView.ViewParameters.isShowingPrice,
    [HEADER_ABBR_ALTAR] = PerksView.ViewParameters.isShowingSourceAltar,
    [HEADER_ABBR_CORNERSTONE] = PerksView.ViewParameters.isShowingSourceCornerstone,
    [HEADER_ABBR_ORDER] = PerksView.ViewParameters.isShowingSourceOrder,
    [HEADER_ABBR_RELIC] = PerksView.ViewParameters.isShowingSourceRelic,
    [HEADER_ABBR_TRADER] = PerksView.ViewParameters.isShowingSourceTrader
}

---getCheckerMethod
---@param headerLabel string the label displayed on the table
---@return function the function to call to see whether that column should be shown
function PerksView.ViewParameters.getCheckerMethod(headerLabel)
    return checkerSwitch[headerLabel]
end

---constructViewParametersFromTemplateFrame
---@param frame table the mediawiki template's calling frame
---@return table an instance of the ViewParameters class
function PerksView.constructViewParametersFromTemplateFrame(frame)
    local newInstance = {}
    newInstance[ARG_SKIP_SOURCES] = frame.args[ARG_SKIP_SOURCES] or "not by default"
    newInstance[ARG_SHOW_ID] = frame.args[ARG_SHOW_ID] or "not by default"
    newInstance[ARG_SHOW_RARITY] = frame.args[ARG_SHOW_RARITY] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_DESCRIPTION] = frame.args[ARG_SHOW_DESCRIPTION] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SOURCE_ALTAR] = frame.args[ARG_SHOW_SOURCE_ALTAR] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SOURCE_CORNERSTONE] = frame.args[ARG_SHOW_SOURCE_CORNERSTONE] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SOURCE_ORDER] = frame.args[ARG_SHOW_SOURCE_ORDER] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SOURCE_RELIC] = frame.args[ARG_SHOW_SOURCE_RELIC] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_SOURCE_TRADER] = frame.args[ARG_SHOW_SOURCE_TRADER] or ARG_SHOW_FLAG_VALUE
    newInstance[ARG_SHOW_PRICE] = frame.args[ARG_SHOW_PRICE] or ARG_SHOW_FLAG_VALUE

    -- Attach methods to the instance
    setmetatable(newInstance, { __index = PerksView.ViewParameters })

    return newInstance
end

--endregion



--region Private member variables

local htmlTable

--endregion



--region Private methods

---createCaptionNode
---@param captionText string the desired caption
---@return table the html node of the caption tags
local function createCaptionNode(captionText)

    captionNode = mw.html.create("caption")

    if null == captionText or "" == captionText then
        captionNode:wikitext(DEFAULT_CAPTION)
    else
        captionNode:wikitext(captionText)
    end

    return captionNode
end

---openTable
---@param caption string the desired caption
---@return table the complete html node, htmlTable, the class variable
local function openTable(caption)

    htmlTable = mw.html.create("table")
    htmlTable:addClass(CLASS_PERKS_TABLE):newline()
    htmlTable:node(createCaptionNode(caption)):newline()

    return htmlTable
end

---createHeaderCell
---@param label string the label to put in the header cell
---@return table a new html table header cell
local function createHeaderCell(label)

    local cell = mw.html.create("th")
    cell:wikitext(label)

    -- Assign the methods so they can be chained
    cell.spanTwoRows = function(self)
        self:attr({ rowspan="2" })
        return self
    end
    cell.makeUnsortable = function(self)
        self:addClass(CLASS_UNSORTABLE)
        return self
    end
    cell.spanMultipleColumns = function(self, columnsToSpan)
        self:attr({ colspan=columnsToSpan })
        return self
    end


    return cell
end

---loopThroughHeaderCells
---@param row table the html table row node to use
---@param preconfiguredRowToUse table the row already setup to choose from
---@param viewParameters table the ViewParameters specified by the author
---@return table the same row, now with more stuff in it
local function addAllCellsThatShouldBeShowing(row, preconfiguredRowToUse, viewParameters)

    for _, rowItem in ipairs(preconfiguredRowToUse) do
        local isShowingIn = PerksView.ViewParameters.getCheckerMethod(rowItem.label)
        if isShowingIn(viewParameters) then
            row:node(rowItem.cell):newline()
        end
    end
end

---addHeaderRow adds header rows to the member variable htmlTable and returns what was added.
---
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table, table the header row added, sometimes a second sub-header row
local function addHeaderRow(viewParameters)

    -- Configure the options; we'll figure out which to create in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
    local headersInOrderWithoutSources = {
        { label = HEADER_ID, cell = createHeaderCell(HEADER_ID) },
        { label = HEADER_NAME, cell = createHeaderCell(HEADER_NAME) },
        { label = HEADER_RARITY, cell = createHeaderCell(HEADER_RARITY) },
        { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):makeUnsortable() },
        { label = HEADER_PRICE, cell = createHeaderCell(HEADER_PRICE) }
    }
    local headersInOrderWithSources = {
        { label = HEADER_ID, cell = createHeaderCell(HEADER_ID):spanTwoRows() },
        { label = HEADER_NAME, cell = createHeaderCell(HEADER_NAME):spanTwoRows() },
        { label = HEADER_RARITY, cell = createHeaderCell(HEADER_RARITY):spanTwoRows() },
        { label = HEADER_DESCRIPTION, cell = createHeaderCell(HEADER_DESCRIPTION):spanTwoRows():makeUnsortable() },
        { label = HEADER_SOURCES, cell = createHeaderCell(HEADER_SOURCES):spanMultipleColumns(viewParameters:countShownSourceColumns()) },
        { label = HEADER_PRICE, cell = createHeaderCell(HEADER_PRICE):spanTwoRows() }
    }
    local subheadersInOrderUnderSources = {
        { label = HEADER_ABBR_ALTAR, cell = createHeaderCell(HEADER_ABBR_ALTAR) },
        { label = HEADER_ABBR_CORNERSTONE, cell = createHeaderCell(HEADER_ABBR_CORNERSTONE) },
        { label = HEADER_ABBR_ORDER, cell = createHeaderCell(HEADER_ABBR_ORDER) },
        { label = HEADER_ABBR_RELIC, cell = createHeaderCell(HEADER_ABBR_RELIC) },
        { label = HEADER_ABBR_TRADER, cell = createHeaderCell(HEADER_ABBR_TRADER) }
    }

    -- If there are subheaders, populate two rows then add them to the main htmlTable
    if viewParameters:isShowingSources() then

        local headerRow = mw.html.create("tr")
        headerRow:newline()
        addAllCellsThatShouldBeShowing(headerRow, headersInOrderWithSources, viewParameters)

        local subheaderRow = mw.html.create("tr")
        subheaderRow:newline()
        addAllCellsThatShouldBeShowing(subheaderRow, subheadersInOrderUnderSources, viewParameters)

        htmlTable:node(headerRow):newline()
        htmlTable:node(subheaderRow):newline()

        return headerRow, subheaderRow

    else
        -- No subheaders, just populate one row then add it to the main htmlTable.
        local headerRow = mw.html.create("tr")
        headerRow:newline()
        addAllCellsThatShouldBeShowing(headerRow, headersInOrderWithoutSources, viewParameters)

        htmlTable:node(headerRow):newline()

        return headerRow
    end
end

---createDataCell
---@param label string the label to put in the cell
---@return table a new html table data cell
local function createDataCell(label)

    local cell = mw.html.create("td")
    cell:wikitext(label)
    return cell
end

---addDataRow
---@param data table a table of data configured with keys to match the labels in cellsInOrder in this method
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table the data row added, sometimes a second sub-header row
 local function addDataRow(data, viewParameters)

    -- Configure the table; we'll figure out which to actually show in a moment. Separating the setup from the logic makes this significantly easier to read and understand. These cannot be key-value pairs, like [label] = cell, because we have to use numerical indexes to preserve their order.
    local cellsInOrderWithoutSources = {
        { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
        { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
        { label = HEADER_RARITY, cell = createDataCell(data[HEADER_RARITY] or "—") },
        { label = HEADER_DESCRIPTION, cell = createDataCell(data[HEADER_DESCRIPTION] or "—") },
        { label = HEADER_PRICE, cell = createDataCell(data[HEADER_PRICE] or "—") }
    }
    local cellsInOrderWithSources = {
        { label = HEADER_ID, cell = createDataCell(data[HEADER_ID] or "—") },
        { label = HEADER_NAME, cell = createDataCell(data[HEADER_NAME] or "—") },
        { label = HEADER_RARITY, cell = createDataCell(data[HEADER_RARITY] or "—") },
        { label = HEADER_DESCRIPTION, cell = createDataCell(data[HEADER_DESCRIPTION] or "—") },
        { label = HEADER_ABBR_ALTAR, cell = createDataCell(data[HEADER_ABBR_ALTAR] and HEADER_ABBR_ALTAR or RED_X) },
        { label = HEADER_ABBR_CORNERSTONE, cell = createDataCell(data[HEADER_ABBR_CORNERSTONE] and HEADER_ABBR_CORNERSTONE or RED_X) },
        { label = HEADER_ABBR_ORDER, cell = createDataCell(data[HEADER_ABBR_ORDER] and HEADER_ABBR_ORDER or RED_X) },
        { label = HEADER_ABBR_RELIC, cell = createDataCell(data[HEADER_ABBR_RELIC] and HEADER_ABBR_RELIC or RED_X) },
        { label = HEADER_ABBR_TRADER, cell = createDataCell(data[HEADER_ABBR_TRADER] and HEADER_ABBR_TRADER or RED_X) },
        { label = HEADER_PRICE, cell = createDataCell(data[HEADER_PRICE] or "—") }
    }

    -- If skipping the sources altogether overrides individual column visibility.
    if viewParameters:isShowingSources() then

        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithSources, viewParameters)

        htmlTable:node(dataRow):newline()

        return dataRow

    else
        local dataRow = mw.html.create("tr")
        dataRow:newline()
        addAllCellsThatShouldBeShowing(dataRow, cellsInOrderWithoutSources, viewParameters)

        htmlTable:node(dataRow):newline()

        return dataRow

    end
end

---validateStringData
---@param stringData string a string to display
---@return string the same string, or nil if it's empty
local function validateStringData(stringData)

    if "" == stringData or " " == stringData then
        return nil
    else
        return stringData
    end
end

---validateBooleanData
---@param booleanData boolean a boolean to display
---@return boolean the same boolean, or false if it's anything besides a true value
local function validateBooleanData(booleanData)

    if true == booleanData or "true" == booleanData then
        return true
    else
        return nil
    end
end

---validateNumberData
---@param numberData number a number to display
---@return number the same number, or nil if it was invalid or 0
local function validateNumberData(numberData)

    if type(numberData) ~= "number" then
        return tonumber(numberData)
    else
        if 0 > numberData then
            return nil
        else
            return numberData
        end
    end
end

local function createPerkLink(id)

    return mw.getCurrentFrame():expandTemplate{
        title = "pl",
        args = { ["id"] = id, ["iconsize"] = PERK_LINK_ICON_SIZE }
    }
end

--endregion



--region Public methods

---startTable
---@param caption string the desired caption
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table an html node, the htmlTable member variable
function PerksView.startTable(caption, viewParameters)

    openTable(caption)
    addHeaderRow(viewParameters)

    return htmlTable
end

---addRow
---@param id string the ID of the perk
---@param _ string the name of the perk is not actually used, but asked for because the Controller doesn't need to know
---@param rarity string the rarity of the perk expressed as a string
---@param description string the description of the perk
---@param isSourceAltar boolean whether the perk is acquired at the altar
---@param isSourceCornerstone boolean whether the perk is acquired as a cornerstone
---@param isSourceOrder boolean whether the perk is acquired from orders
---@param isSourceRelic boolean whether the perk is acquired from relics
---@param isSourceTrader boolean whether the perk is acquired from traders
---@param price number the purchase price at a trader
---@param viewParameters table a ViewParameters initialized with its constructor
---@return table an html node, the htmlTable member variable
function PerksView.addRow(id, _, rarity, description, isSourceAltar, isSourceCornerstone, isSourceOrder, isSourceRelic, isSourceTrader, price, viewParameters)

    local data = {
        [HEADER_ID] = validateStringData(id),
        [HEADER_NAME] = createPerkLink(validateStringData(id)),
        [HEADER_RARITY] = validateStringData(rarity),
        [HEADER_DESCRIPTION] = validateStringData(description),
        [HEADER_ABBR_ALTAR] = validateBooleanData(isSourceAltar),
        [HEADER_ABBR_CORNERSTONE] = validateBooleanData(isSourceCornerstone),
        [HEADER_ABBR_ORDER] = validateBooleanData(isSourceOrder),
        [HEADER_ABBR_RELIC] = validateBooleanData(isSourceRelic),
        [HEADER_ABBR_TRADER] = validateBooleanData(isSourceTrader),
        [HEADER_PRICE] = validateNumberData(price)
    }

    addDataRow(data, viewParameters)

    return htmlTable
end

---finalize
---@return table the complete html table node, fully rendered
function PerksView.finalize()

    return htmlTable
end

--endregion

return PerksView