Module:PerksView

From Against the Storm Official Wiki
Revision as of 21:06, 23 June 2024 by Aeredor (talk | contribs) (adding finalize method to return the completed markup)

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 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 CLASS_PERKS_TABLE = "wikitable sortable mw-collapsible"
local CLASS_UNSORTABLE = "unsortable"

local ATTR_SPAN_TWO_ROWS = { rowspan="2" }

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

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 ARG_SKIP_FLAG_VALUE
    newInstance[ARG_SHOW_ID] = frame.args[ARG_SHOW_ID] 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_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(ATTR_SPAN_TWO_ROWS)
        return self
    end
    cell.makeUnsortable = function(self)
        self:addClass(CLASS_UNSORTABLE)
        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) },
        { 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 cellsInOrder = {
        { 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 "—") }
    }

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

    htmlTable:node(dataRow):newline()

    return dataRow
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

--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 name string the name of the perk
---@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, name, rarity, description, isSourceAltar, isSourceCornerstone, isSourceOrder, isSourceRelic, isSourceTrader, price, viewParameters)

    local data = {
        [HEADER_ID] = validateStringData(id),
        [HEADER_NAME] = validateStringData(name),
        [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 string the complete view fully rendered
function PerksView.finalize()
    return tostring(htmlTable)
end

--endregion

return PerksView