Jump to content

Module:Wiki output utilities

From HopperWiki

Documentation for this module may be created at Module:Wiki output utilities/doc

--[[
Module:Wiki output utilities
Part of the Module:Utilities family.

All forward-facing HTML and wikitext renderers. Receives already-processed
data and turns it into output strings. No Cargo queries here.
All functions are re-exported through Module:Utilities for backward compatibility.

Functions:
  warning_box                      Render a styled warning box
  generate_wiki_table              Generate a wiki table (legacy + enhanced modes)
  generate_wiki_table_enhanced     Generate a wiki table with collapsible row support
  generate_wiki_table_simple       Generate a plain wiki table with minimal options
  build_bulleted_taxon_list        Render a nested taxon hierarchy as a bulleted HTML list
  display_field_in_bulleted_list   Render a bulleted list from a Cargo results field
  bulleted_list                    Render a sorted bulleted list of unique field values
--]]


local output = {}


-- ============================================================
-- Warning box
-- ============================================================

-- Renders a styled mw-message-box warning div with a bold title and body text.
function output.warning_box(title, msg)
    local box = mw.html.create("div")
        :addClass("mw-message-box mw-message-box-warning")
        :cssText("margin:0.8em 0;")
    box:tag("strong"):wikitext(title):done()
    box:tag("div")
        :cssText("margin-top:0.4em; white-space:pre-wrap;")
        :wikitext(msg)
    return tostring(box)
end


-- ============================================================
-- generate_wiki_table
-- ============================================================

--[[
Generates a formatted wiki table from Cargo query results.
Supports two modes:

Legacy / compat mode (default):
  Accepts a flat array of pre-formatted rows and a simple display_fields list.
  Outputs raw field values as-is.

Enhanced mode (compat_mode = false, field_schema provided):
  Formats fields by declared Cargo type (page, file, list variants).
  Supports column display name mapping and character cutoff.

Both modes support optional collapsible table behaviour via mw-collapsible.
--]]
function output.generate_wiki_table(rows, display_fields, collapse, character_cutoff, field_schema, column_label_map, compat_mode, file_display_format)

    -- Detect legacy argument order: (rows, header, display_fields, collapse, ...)
    if type(display_fields) == "string" and type(collapse) == "table" then
        local header = display_fields
        display_fields    = collapse
        collapse          = character_cutoff
        character_cutoff  = field_schema
        field_schema      = nil
        column_label_map  = nil
        compat_mode       = true
        file_display_format = nil
    end

    file_display_format = file_display_format or "[[File:%s|frameless|100px]]"
    if compat_mode == nil then compat_mode = true end

    -- ----------------------------------------
    -- Legacy mode
    -- ----------------------------------------
    if compat_mode or type(field_schema) ~= "table" then
        local html = mw.html.create("table")
            :addClass("wikitable")
            :addClass("cargo-table")
            :addClass("sortable")

        local thead = html:tag("tr")
        for _, field in ipairs(display_fields) do
            local label = column_label_map and column_label_map[field] or field
            thead:tag("th"):wikitext(label)
        end

        for _, row in ipairs(rows) do
            local tr = html:tag("tr")
            for _, field in ipairs(display_fields) do
                tr:tag("td"):wikitext(row[field] or "")
            end
        end

        if collapse then
            html:addClass("mw-collapsible")
            html:addClass("mw-collapsed")
        end

        return tostring(html)
    end

    -- ----------------------------------------
    -- Enhanced mode
    -- ----------------------------------------
    local html = mw.html.create("table")
        :addClass("wikitable")
        :addClass("cargo-table")
        :addClass("sortable")

    local thead = html:tag("tr")
    for _, field in ipairs(display_fields) do
        local display_name = (column_label_map and column_label_map[field]) or field
        thead:tag("th"):wikitext(display_name)
    end

    for _, row in ipairs(rows) do
        local tr = html:tag("tr")
        for _, field in ipairs(display_fields) do
            local value      = row[field]
            local field_info = field_schema[field]
            local field_type = field_info and field_info.type or "string"
            local content    = ""

            if type(value) == "table" then
                local formatted_items = {}
                for _, item in ipairs(value) do
                    if field_type == "list_of_page" then
                        table.insert(formatted_items, "[[" .. item .. "]]")
                    elseif field_type == "list_of_file" then
                        if item and item ~= "" then
                            table.insert(formatted_items, string.format(file_display_format, item))
                        end
                    else
                        table.insert(formatted_items, item)
                    end
                end
                content = table.concat(formatted_items, ", ")
            elseif type(value) == "string" then
                if field_type == "page" then
                    content = "[[" .. value .. "]]"
                elseif field_type == "file" then
                    content = (value and value ~= "") and string.format(file_display_format, value) or ""
                else
                    content = value
                end
            end

            if character_cutoff
                and field_type ~= "file"
                and field_type ~= "list_of_file"
                and mw.ustring.len(content) > character_cutoff then
                content = mw.ustring.sub(content, 1, character_cutoff) .. "..."
            end

            tr:tag("td"):wikitext(content)
        end
    end

    if collapse then
        html:addClass("mw-collapsible")
        html:addClass("mw-collapsed")
    end

    return tostring(html)
end


-- ============================================================
-- generate_wiki_table_enhanced
-- ============================================================

--[[
Generates a wiki table using wikitext string assembly rather than
mw.html. Supports a custom collapsible row cutoff via CSS classes.
--]]
function output.generate_wiki_table_enhanced(
    rows, display_fields, collapse, character_cutoff,
    field_schema, column_label_map, compat_mode,
    file_display_format, row_cutoff, options
)
    file_display_format = file_display_format or "[[File:%s|frameless|100px]]"
    compat_mode = (compat_mode == nil) and true or compat_mode
    local row_cutoff_value = row_cutoff or 5
    options = options or {}

    local classes = { "wikitable", "cargo-table", "sortable" }
    if collapse then
        table.insert(classes, "collapsible-cargo-table")
        table.insert(classes, "row-cutoff-" .. row_cutoff_value)
    end

    local lines = {}
    table.insert(lines, '{| class="' .. table.concat(classes, " ") .. '"')

    table.insert(lines, "|-")
    for _, field in ipairs(display_fields) do
        local display_name = (column_label_map and column_label_map[field]) or field
        table.insert(lines, "! " .. display_name)
    end

    for _, row in ipairs(rows) do
        table.insert(lines, "|-")
        for _, field in ipairs(display_fields) do
            local value      = row[field]
            local field_info = field_schema and field_schema[field]
            local field_type = field_info and field_info.type or "string"
            local content    = ""

            if type(value) == "table" then
                local formatted_items = {}
                for _, item in ipairs(value) do
                    if field_type == "list_of_page" then
                        table.insert(formatted_items, "[[" .. item .. "]]")
                    elseif field_type == "list_of_file" then
                        if item and item ~= "" then
                            table.insert(formatted_items, string.format(file_display_format, item))
                        end
                    else
                        table.insert(formatted_items, item)
                    end
                end
                content = table.concat(formatted_items, ", ")
            elseif type(value) == "string" then
                if field_type == "page" then
                    content = "[[" .. value .. "]]"
                elseif field_type == "file" then
                    content = (value and value ~= "") and string.format(file_display_format, value) or ""
                else
                    content = value
                end
            end

            if character_cutoff
                and field_type ~= "file"
                and field_type ~= "list_of_file"
                and mw.ustring.len(content) > character_cutoff then
                content = mw.ustring.sub(content, 1, character_cutoff) .. "..."
            end

            table.insert(lines, "| " .. (content or ""))
        end
    end

    table.insert(lines, "|}")
    return table.concat(lines, "\n")
end


-- ============================================================
-- generate_wiki_table_simple
-- ============================================================

--[[
Generates a plain wiki table with minimal options. No schema required.
Useful for simple pre-formatted result arrays.
--]]
function output.generate_wiki_table_simple(
    rows,
    display_fields,
    column_label_map,
    collapse,
    character_cutoff,
    row_cutoff,
    extra_classes
)
    character_cutoff = character_cutoff or nil
    row_cutoff       = row_cutoff or 5
    extra_classes    = extra_classes or {}

    local classes = { "wikitable", "cargo-table", "sortable" }
    if collapse then
        table.insert(classes, "collapsible-cargo-table")
        table.insert(classes, "row-cutoff-" .. row_cutoff)
    end
    for _, cls in ipairs(extra_classes) do
        table.insert(classes, cls)
    end

    local lines = {}
    table.insert(lines, '{| class="' .. table.concat(classes, " ") .. '"')

    table.insert(lines, "|-")
    for _, field in ipairs(display_fields) do
        local label = (column_label_map and column_label_map[field]) or field
        table.insert(lines, "! " .. label)
    end

    for _, row in ipairs(rows) do
        table.insert(lines, "|-")
        for _, field in ipairs(display_fields) do
            local value   = row[field]
            local content = ""
            if value ~= nil then
                content = tostring(value)
                if character_cutoff and mw.ustring.len(content) > character_cutoff then
                    content = mw.ustring.sub(content, 1, character_cutoff) .. "..."
                end
            end
            table.insert(lines, "| " .. content)
        end
    end

    table.insert(lines, "|}")
    return table.concat(lines, "\n")
end


-- ============================================================
-- build_bulleted_taxon_list
-- ============================================================

--[[
Renders a nested taxon hierarchy (produced by tax.build_nested_hierarchy)
as a bulleted HTML unordered list. Only the children of the root node
are rendered -- the root itself is not shown.
--]]
function output.build_bulleted_taxon_list(taxon_hierarchy)
    local function recurse(node)
        local list = mw.html.create("ul")
        for _, child in ipairs(node.children or {}) do
            local li = list:tag("li")
            if child.rank ~= "" then
                li:wikitext(child.rank .. ": " .. child.name)
            else
                li:wikitext(child.name)
            end
            if #child.children > 0 then
                li:node(recurse(child))
            end
        end
        return list
    end

    return tostring(recurse(taxon_hierarchy))
end


-- ============================================================
-- display_field_in_bulleted_list
-- ============================================================

--[[
Generates a bulleted HTML list from a single field across all Cargo rows.
Handles multi-value fields by splitting on delimiter.

args:
  cargo_results       -- Cargo results array
  field_name          -- field to extract values from
  sort_alphabetically -- boolean (default false)
  page_links          -- boolean, wrap items in wiki page links (default false)
  delimiter           -- split delimiter (default ",")
--]]
function output.display_field_in_bulleted_list(args)
    local cargo_results       = args.cargo_results
    local field_name          = args.field_name
    local sort_alphabetically = args.sort_alphabetically or false
    local page_links          = args.page_links or false
    local delimiter           = args.delimiter or ","

    local items = {}

    for _, result in ipairs(cargo_results) do
        local field_value = result[field_name] or ""
        local item_list   = mw.text.split(field_value, delimiter)
        for _, item in ipairs(item_list) do
            local trimmed = mw.text.trim(item)
            if trimmed ~= "" then
                if page_links then
                    trimmed = string.format("[[%s]]", trimmed)
                end
                table.insert(items, trimmed)
            end
        end
    end

    if sort_alphabetically then
        table.sort(items, function(a, b)
            local clean_a = mw.ustring.gsub(a, "^%[%[(.-)%]%]$", "%1")
            local clean_b = mw.ustring.gsub(b, "^%[%[(.-)%]%]$", "%1")
            return clean_a < clean_b
        end)
    end

    local ul = mw.html.create("ul")
    for _, item in ipairs(items) do
        ul:tag("li"):wikitext(item)
    end

    return tostring(ul)
end


-- ============================================================
-- bulleted_list
-- ============================================================

--[[
Generates a sorted bulleted list of unique values for a specified field.
Handles both scalar string values and list-type (table) values.
Wraps "page" and "list_of_page" fields in wiki page links. Set
field_schema[field_name].italicize = true to also wrap entries in wiki
italics (e.g. for scientific names).
--]]
function output.bulleted_list(results, field_name, field_schema, column_label_map)
    field_schema = field_schema or {}
    local uniques = {}

    for _, row in ipairs(results) do
        local value = row[field_name]
        if type(value) == "table" then
            for _, v in ipairs(value) do
                uniques[v] = true
            end
        elseif type(value) == "string" then
            uniques[value] = true
        end
    end

    local sorted = {}
    for v, _ in pairs(uniques) do
        table.insert(sorted, v)
    end
    table.sort(sorted)

    local field_info = field_schema[field_name]
    local field_type = field_info and field_info.type or "string"
    local italicize = field_info and field_info.italicize

    local list = {}
    for _, item in ipairs(sorted) do
        local display = item
        if field_type == "page" or field_type == "list_of_page" then
            display = "[[" .. display .. "]]"
        end
        if italicize then
            display = "''" .. display .. "''"
        end
        table.insert(list, mw.html.create("li"):wikitext(display))
    end

    if #list > 0 then
        local result_list = mw.html.create("ul")
        for _, li in ipairs(list) do
            result_list:node(li)
        end
        return tostring(result_list) .. "\n"
    else
        return nil
    end
end


return output
Cookies help us deliver our services. By using our services, you agree to our use of cookies.