Module:Item infobox

From Space Station 14 Wiki
Revision as of 14:11, 8 May 2025 by Aliser (talk | contribs) (fix 'edible by' showing up on infoboxes with all of the related 'edible by' fields turned off)
Module documentation
View or edit this documentation (about module documentation)

Contains partial implementation for {{Item infobox}}.

Edible by params

Edible by parameters are implemented in this module, entrypoint generate_edible_by.

List of edible by params is defined by edible_by_display_tbl.

Tests

Module:Item infobox/testcases/edible by

✅ All tests passed.

Name Expected Actual
test_empty
test_everyone
test_everyone_except_other_options
test_everyone_except_reptilians
test_moth
test_reptilian

local p = {}
local getArgs = require('Module:Arguments').getArgs
local yesNo = require('Module:Yesno')

-- ==========================

local edible_by_rest_param_name_lc = "edible by rest"

-- A list of edible by parameters along with the texts to display for each.
-- Param names defined here must be lowercase.
--
-- "edible by rest" is a special case, hence the table format.
-- * If all options are selected, including "edible by rest" (ie food is edible by everyone),
-- then the first form will be used.
-- * If only some options are selected, including "edible by rest",
-- then the second form will be used, following by a list of unselected options.
local edible_by_display_tbl = {
    { param = "edible by reptilians",       display = "Reptilians" },
    { param = "edible by moths",            display = "Moth people" },
    { param = edible_by_rest_param_name_lc, display = { "Everyone", "Everyone, except:" } },
}

-- ==========================

local function assert_not_nil(value, error_message)
    if value == nil then
        if error_message == nil then
            error("value is nil")
        else
            error(error_message)
        end
    end
end

local function starts_with(str, substr)
    return string.sub(str, 1, string.len(substr)) == substr
end

local function ends_with(str, substr)
    local substr_length = string.len(substr)
    return string.sub(str, string.len(str) - substr_length + 1, string.len(str)) == substr
end

local function starts_with_insensitive(str, substr)
    return starts_with(string.lower(str), string.lower(substr))
end

local function ends_with_insensitive(str, substr)
    return ends_with(string.lower(str), string.lower(substr))
end

local function table_filter(tbl, filterFn)
    local out = {}

    for k, v in pairs(tbl) do
        if filterFn(v, k, tbl) then out[k] = v end
    end

    return out
end

local function table_find(tbl, findFn)
    for k, v in pairs(tbl) do
        if findFn(v, k, tbl) then return v end
    end
end

local function table_contains(tbl, findFn)
    return table_find(tbl, findFn) ~= nil
end

local function table_contains_value(tbl, value)
    return table_find(tbl, function(iter_value)
        return iter_value == value
    end) ~= nil
end

local function table_find_key(tbl, findFn)
    for k, v in pairs(tbl) do
        if findFn(v, k, tbl) then return k end
    end
end

local function table_find_index(tbl, findFn)
    for i, v in ipairs(tbl) do
        if findFn(v, i, tbl) then return i end
    end
end

local function table_map(tbl, mapFn)
    local res = {}
    for k, v in pairs(tbl) do
        table.insert(res, mapFn(v, k, tbl))
    end
    return res
end

local function numeric_table_length(t)
    local count = 0
    for _ in ipairs(t) do count = count + 1 end
    return count
end

local function table_length(t)
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end


-- ==========================

function p.generate_edible_by(frame)
    local args = getArgs(frame)

    local edible_by_args = table_filter(args, function(value, key)
        return starts_with_insensitive(key, "edible by")
    end)

    -- number of "false" edible by args
    local edible_by_args_true_count = table_length(
        table_filter(edible_by_args, function(value, key)
            return yesNo(value)
        end)
    )

    if table_length(edible_by_args) == 0 or edible_by_args_true_count == 0 then
        return ""
    end

    local edible_by_rest_key = table_find_key(edible_by_args, function(value, key)
        return ends_with_insensitive(key, "rest")
    end)

    -- save the edible by rest value to use later
    local is_edible_by_rest = yesNo(edible_by_args[edible_by_rest_key])

    -- generate a temp table made up only of numerical params to make it sortable.
    -- get rid of unselected "edible by"s.
    local selected_params_lc = {}
    for key, value in pairs(edible_by_args) do
        -- iterate over each key except edible by rest
        if key ~= edible_by_rest_key then
            -- check if its selected
            if yesNo(value) then
                table.insert(selected_params_lc, string.lower(key))
            end
        end
    end

    -- searches for a param name in the params table,
    -- returns the index ("order") of that entry.
    -- throws if no param was found (ie not defined in the table).
    local function find_edible_by_order(param_name_lc)
        local index = table_find_index(edible_by_display_tbl, function(value)
            return value.param == param_name_lc
        end)

        assert_not_nil(index, "failed to find edible by order: param name '" ..
            param_name_lc .. "' if not defined in the edible by params table")

        return index
    end

    -- sort the temp table based on predefine order
    table.sort(selected_params_lc, function(a, b)
        return find_edible_by_order(a) > find_edible_by_order(b)
    end)

    -- render out the params
    local edible_by_rest_entry = is_edible_by_rest
        and table_find(edible_by_display_tbl, function(value)
            return value.param == edible_by_rest_param_name_lc
        end)

    if is_edible_by_rest and edible_by_rest_entry == nil then
        error(
            "failed to generate edible by: edible by rest param is enabled, but it is not defined in the display table")
    end

    local selected_params_lc_len = numeric_table_length(selected_params_lc)
    -- count total selected "edible by" params, including the edible by rest
    local total_selected_params = selected_params_lc_len + (is_edible_by_rest and 1 or 0)
    local display_table_len = numeric_table_length(edible_by_display_tbl)

    local list_el = mw.html.create('ul')

    local function append_li(text)
        list_el
            :tag("li")
            :wikitext(text) -- relates to the li el, not the list el
    end

    local function append_li_and_subli(li_text, subli_els)
        local li_el = mw.html.create("li")
            :wikitext(li_text)

        local subli_ul = mw.html.create('ul')
        for _, node in ipairs(subli_els) do
            subli_ul
                :node(node)
        end

        li_el:
            node(subli_ul)

        list_el
            :node(li_el)
    end

    if total_selected_params == display_table_len then
        -- all params selected, edible by everyone
        -- edible by rest is assumed to be on implicitly, cz otherwise the count won't match.
        append_li(edible_by_rest_entry.display[1])
    else
        if is_edible_by_rest then
            -- if edible by contains the rest option, we compile a list of those excluded from the display table.
            -- those selected are excluded from the compiled list.

            -- compile a list of those potentially excluded
            local excluded_pool = table_filter(edible_by_display_tbl, function(entry)
                return entry.param ~= edible_by_rest_param_name_lc
            end)

            -- find whose excluded (not selected)
            local excluded = table_filter(excluded_pool, function(entry)
                return not table_contains_value(selected_params_lc, entry.param)
            end)

            -- format em as li els
            local excluded_as_li_els = table_map(excluded, function(value)
                return mw.html.create("li")
                    :wikitext(value.display)
            end)

            append_li_and_subli(edible_by_rest_entry.display[2], excluded_as_li_els)
        else
            -- if edible by doesn't contain the rest option, then its a regular list

            for _, param_name_lc in ipairs(selected_params_lc) do
                local entry = table_find(edible_by_display_tbl, function(entry)
                    return entry.param == param_name_lc
                end)

                assert_not_nil(entry, "failed to generate edible by param: param '" ..
                    param_name_lc .. "' (lowercase) is not defined in the edible by display table")

                append_li(entry.display)
            end
        end
    end

    return tostring(list_el)
end

return p