Jump to content

Module:Cargo format utilities

From HopperWiki

Documentation for this module may be created at Module:Cargo format utilities/doc

--[[
Module:Cargo format utilities
Part of the Module:Utilities family.

Transforms raw Cargo query results into display-ready values.
No direct Cargo queries here -- this module only receives and reshapes
result tables produced upstream.
All functions are re-exported through Module:Utilities for backward compatibility.

Functions:
  format_cargo_results                 Format a Cargo results array using a flat field_formats table
  format_cargo_results_with_schema     Format a Cargo results array using a JSON schema table
  format_results_with_schema           Format results using a schema table or base page name
  parse_cargo_results                  Normalize raw Cargo results into typed Lua structures
  parse_arguments                      Parse a delimited string into formatted wiki text
  transform_link_fields                Rewrite URL fields as labeled wiki external links
  cargo_row_remover                    Remove rows whose field value contains a substring
  extract_top_cargo_row                Pull field values from the first row of a result
--]]


local cargo_fmt = {}


-- ============================================================
-- Internal helpers
-- ============================================================

local function add_file_prefix_gentle(file_name)
    if not file_name or file_name == "" then return "" end
    file_name = mw.text.trim(file_name)
    local inner_file = mw.ustring.match(file_name, "^%[%[File:([^|%]]+)")
    if inner_file then
        return "File:" .. mw.text.trim(inner_file)
    end
    if not mw.ustring.match(file_name, "^[Ff]ile:") then
        file_name = "File:" .. file_name
    end
    return file_name
end


-- ============================================================
-- parse_arguments
-- ============================================================

--[[
Parses a delimited string (comma or ~~) into a formatted string of items.

  format = "page"     -- wrap each item as a wiki page link
  format = "ext_link" -- wrap each item as an external link
  link_text           -- label for external links (default "Link")
--]]
function cargo_fmt.parse_arguments(text, format, link_text)
    if not text or text == "" then return "" end

    local function is_wiki_link(item)
        return item:match("^%[%[.-%]%]$") ~= nil
    end

    local delimiter = text:find("~~", 1, true) and "~~" or ","
    local parts = mw.text.split(text, delimiter, true)

    local results = {}
    for _, raw in ipairs(parts) do
        local item = mw.text.trim(raw)
        if item ~= "" then
            if format == "page" then
                if not is_wiki_link(item) then
                    item = "[[" .. item .. "]]"
                end
            elseif format == "ext_link" then
                if item:match("^https?://") then
                    item = "[" .. item .. " " .. (link_text or "Link") .. "]"
                elseif not item:match("^%[https?://.- .-%]$") then
                    item = "[" .. item .. " " .. (link_text or "Link") .. "]"
                end
            end
            table.insert(results, item)
        end
    end

    return table.concat(results, ", ")
end


-- ============================================================
-- format_cargo_results
-- ============================================================

--[[
Formats a raw Cargo results array using a flat field_formats table.
Supported format values per field:
  "string", "list_of_string", "page", "list_of_page", "url", "file"

file_display_format controls how file fields are rendered, e.g.
"frameless|100px" -- defaults to that if nil.
--]]
function cargo_fmt.format_cargo_results(cargo_results, field_formats, field_name_mappings, file_display_format)
    file_display_format = file_display_format or "[[%s|frameless|100px]]"
    local formatted_results = {}
    field_name_mappings = field_name_mappings or {}

    local function split_and_format_list(value, delimiter)
        local string_list = mw.text.split(value, delimiter)
        for i, v in ipairs(string_list) do
            string_list[i] = mw.text.trim(v)
        end
        return table.concat(string_list, ", ")
    end

    for _, result in ipairs(cargo_results) do
        local formatted_result = {}

        for field, value in pairs(result) do
            if value and value ~= "" then
                local format = field_formats[field]
                local formatted_value = value

                if format == "list_of_string" then
                    if value:find(";") then
                        formatted_value = split_and_format_list(value, ";")
                    else
                        formatted_value = split_and_format_list(value, ",")
                    end

                elseif format == "page" then
                    formatted_value = "[[" .. value .. "]]"

                elseif format == "list_of_page" then
                    local delimiter = value:find(";") and ";" or ","
                    local page_links = {}
                    local values = mw.text.split(value, delimiter)
                    local current_value = ""
                    for _, v in ipairs(values) do
                        v = mw.text.trim(v)
                        if current_value ~= "" then
                            current_value = current_value .. delimiter .. v
                        else
                            current_value = v
                        end
                        if not v:match("^%s*$") then
                            table.insert(page_links, "[[" .. current_value .. "]]")
                            current_value = ""
                        end
                    end
                    if current_value ~= "" then
                        table.insert(page_links, "[[" .. current_value .. "]]")
                    end
                    formatted_value = table.concat(page_links, ", ")

                elseif format == "url" then
                    formatted_value = string.format("[%s View URL]", value)

                elseif format == "file" then
                    local safe_file_name = add_file_prefix_gentle(value)
                    local is_pdf = safe_file_name:lower():match("%.pdf$")
                    if is_pdf then
                        local pdf_fmt = file_display_format or "[[%s|thumb|100px|link=%s]]"
                        formatted_value = string.format(pdf_fmt, safe_file_name, safe_file_name)
                    else
                        formatted_value = string.format(file_display_format, safe_file_name)
                    end
                end

                local new_field_name = field_name_mappings[field] or field
                formatted_result[new_field_name] = formatted_value
            end
        end

        table.insert(formatted_results, formatted_result)
    end

    return formatted_results
end


-- ============================================================
-- format_cargo_results_with_schema
-- ============================================================

--[[
Formats a Cargo results array using a JSON schema table.
Handles both legacy string values (delimited) and always-array Lua
table values from the JSON pipeline transparently.
--]]
function cargo_fmt.format_cargo_results_with_schema(cargo_results, schema, field_name_mappings, file_display_format)
    file_display_format = file_display_format or "[[%s|frameless|100px]]"
    local formatted_results = {}
    field_name_mappings = field_name_mappings or {}

    local function is_empty(v)
        return v == nil
            or v == ""
            or (type(v) == "table" and next(v) == nil)
    end

    local function split_and_format_list(value, delimiter)
        local string_list = mw.text.split(value, delimiter)
        for i, v in ipairs(string_list) do
            string_list[i] = mw.text.trim(v)
        end
        return table.concat(string_list, ", ")
    end

    local function table_to_string_list(tbl)
        local out = {}
        for _, v in ipairs(tbl) do
            local s = mw.text.trim(tostring(v))
            if s ~= "" then out[#out + 1] = s end
        end
        return out
    end

    for _, result in ipairs(cargo_results) do
        local formatted_result = {}

        for field, value in pairs(result) do
            if not is_empty(value) then
                local field_schema = schema[field]

                if field_schema then
                    local format    = field_schema.type
                    local delimiter = field_schema.delimiter
                    local formatted_value = value
                    local is_table  = type(value) == "table"

                    if format == "list_of_string" then
                        if is_table then
                            formatted_value = table.concat(table_to_string_list(value), ", ")
                        else
                            if delimiter then
                                formatted_value = split_and_format_list(value, delimiter)
                            elseif value:find("~~") then
                                formatted_value = split_and_format_list(value, "~~")
                            elseif value:find(";") then
                                formatted_value = split_and_format_list(value, ";")
                            else
                                formatted_value = split_and_format_list(value, ",")
                            end
                        end

                    elseif format == "list_of_page" then
                        local items
                        if is_table then
                            items = table_to_string_list(value)
                        else
                            local delim = delimiter
                                or (value:find("~~") and "~~")
                                or (value:find(";")  and ";")
                                or ","
                            local parts = mw.text.split(value, delim)
                            items = {}
                            for _, v in ipairs(parts) do
                                local s = mw.text.trim(v)
                                if s ~= "" then items[#items + 1] = s end
                            end
                        end
                        local page_links = {}
                        for _, v in ipairs(items) do
                            page_links[#page_links + 1] = "[[" .. v .. "]]"
                        end
                        formatted_value = table.concat(page_links, ", ")

                    elseif format == "page" then
                        local s = is_table and tostring(value[1] or "") or value
                        formatted_value = s ~= "" and ("[[" .. s .. "]]") or ""

                    elseif format == "url" then
                        local s = is_table and tostring(value[1] or "") or value
                        formatted_value = s ~= "" and string.format("[%s View URL]", s) or ""

                    elseif format == "file" then
                        local s = is_table and tostring(value[1] or "") or value
                        if s ~= "" then
                            local safe_file_name = add_file_prefix_gentle(s)
                            local is_pdf = safe_file_name:lower():match("%.pdf$")
                            if is_pdf then
                                local pdf_fmt = file_display_format or "[[%s|thumb|100px|link=%s]]"
                                formatted_value = string.format(pdf_fmt, safe_file_name, safe_file_name)
                            else
                                formatted_value = string.format(file_display_format, safe_file_name)
                            end
                        else
                            formatted_value = ""
                        end

                    else
                        -- "string" and unrecognised types
                        formatted_value = is_table and tostring(value[1] or "") or value
                    end

                    if not is_empty(formatted_value) then
                        local new_field_name = field_name_mappings[field] or field
                        formatted_result[new_field_name] = formatted_value
                    end
                else
                    -- Field not in schema: pass through unchanged
                    formatted_result[field] = value
                end
            end
        end

        table.insert(formatted_results, formatted_result)
    end

    return formatted_results
end


-- ============================================================
-- format_results_with_schema
-- ============================================================

--[[
Formats results using either a schema table directly, or a base page
name from which the schema is loaded via Module:JSON_pipeline.
--]]
function cargo_fmt.format_results_with_schema(results, base_page_or_schema, field_name_mappings)
    if not results or #results == 0 then return results end

    field_name_mappings = field_name_mappings or {}

    local schema
    if type(base_page_or_schema) == "table" then
        schema = base_page_or_schema
    else
        schema = require("Module:JSON_pipeline").load_schema(base_page_or_schema)
        if not schema then return results end
    end

    local function split_list(value, delimiter)
        local pieces = mw.text.split(value, delimiter)
        for i, v in ipairs(pieces) do
            pieces[i] = mw.text.trim(v)
        end
        return pieces
    end

    local function join_list(list)
        return mw.text.listToText(list)
    end

    local formatted = {}

    for _, row in ipairs(results) do
        local formatted_row = {}

        for field, value in pairs(row) do
            if value and value ~= "" then
                local entry_schema = schema[field]

                if entry_schema then
                    local t     = entry_schema.type
                    local delim = entry_schema.delimiter
                    local new_value = value

                    if t == "list_of_string" or t == "list_of_url" or t == "list_of_page" then
                        local list
                        if type(value) == "table" then
                            list = {}
                            for i, v in ipairs(value) do
                                list[i] = mw.text.trim(tostring(v))
                            end
                        else
                            local d = delim
                                or (value:find("~~") and "~~")
                                or (value:find(";")  and ";")
                                or ","
                            list = split_list(value, d)
                        end

                        if t == "list_of_page" then
                            for i, v in ipairs(list) do
                                list[i] = "[[" .. v .. "]]"
                            end
                        elseif t == "list_of_url" then
                            for i, v in ipairs(list) do
                                list[i] = string.format("[%s link]", v)
                            end
                        end
                        new_value = join_list(list)

                    elseif t == "page" then
                        new_value = "[[" .. value .. "]]"

                    elseif t == "url" then
                        new_value = string.format("[%s View URL]", value)

                    elseif t == "file" then
                        new_value = string.format("[[%s|frameless|100px]]", value)
                    end

                    formatted_row[field_name_mappings[field] or field] = new_value
                else
                    formatted_row[field] = value
                end
            end
        end

        table.insert(formatted, formatted_row)
    end

    return formatted
end


-- ============================================================
-- parse_cargo_results
-- ============================================================

--[[
Normalizes raw Cargo results into a structured table with typed fields.
List fields are split into Lua arrays; scalar fields remain strings.

Returns { results = {...}, field_types = {...} }
--]]
function cargo_fmt.parse_cargo_results(cargo_results, field_schema)
    local parsed_results = {}
    local field_types    = {}

    for _, result in ipairs(cargo_results) do
        local parsed_row = {}

        for field, value in pairs(result) do
            local field_info  = field_schema[field]
            if field_info then
                local field_type  = field_info.type
                local delimiter   = field_info.delimiter or ","

                if field_type:match("^list_of_") then
                    if value == "" then
                        parsed_row[field] = {}
                    else
                        local list = mw.text.split(value, delimiter .. "%s*")
                        for i, v in ipairs(list) do
                            list[i] = mw.text.trim(v)
                        end
                        parsed_row[field] = list
                    end
                    field_types[field] = "list"
                else
                    parsed_row[field]  = value
                    field_types[field] = "string"
                end
            else
                parsed_row[field]  = value
                field_types[field] = "string"
            end
        end

        table.insert(parsed_results, parsed_row)
    end

    return {
        results     = parsed_results,
        field_types = field_types
    }
end


-- ============================================================
-- transform_link_fields
-- ============================================================

--[[
Rewrites URL and list_of_url fields in a results array as labeled
external links. Respects an optional whitelist of fields to transform.

options:
  link_text        -- label text (default "View")
  link_text_fields -- array of field names to transform (default: all url fields)
--]]
function cargo_fmt.transform_link_fields(results, display_fields, field_schema, options)
    options = options or {}
    local link_text = options.link_text or "View"

    local link_text_fields = {}
    if type(options.link_text_fields) == "table" then
        for _, fname in ipairs(options.link_text_fields) do
            link_text_fields[fname] = true
        end
    end

    local transformed = mw.clone(results)

    for _, row in ipairs(transformed) do
        for _, field in ipairs(display_fields) do
            local field_info = field_schema[field]
            if field_info then
                local field_type     = field_info.type
                local is_url         = field_type == "url"
                local is_list_of_url = field_type == "list_of_url"
                local is_target      = not options.link_text_fields or link_text_fields[field]

                if is_target then
                    local value = row[field]
                    if is_url and type(value) == "string" and value ~= "" then
                        row[field] = string.format("[%s %s]", value, link_text)
                    elseif is_list_of_url and type(value) == "table" then
                        local formatted = {}
                        for _, url in ipairs(value) do
                            table.insert(formatted, string.format("[%s %s]", url, link_text))
                        end
                        row[field] = formatted
                    end
                end
            end
        end
    end

    return transformed
end


-- ============================================================
-- cargo_row_remover
-- ============================================================

-- Removes rows from a Cargo results array where field contains value as a substring.
function cargo_fmt.cargo_row_remover(cargo_results, field, value)
    local modified_results = {}
    for _, row in ipairs(cargo_results) do
        if not row[field] or not string.find(row[field], value) then
            table.insert(modified_results, row)
        end
    end
    return modified_results
end


-- ============================================================
-- extract_top_cargo_row
-- ============================================================

--[[
Extracts field values from the first row of a Cargo results array into a
flat table. If fields is nil, all fields from the first row are used.
--]]
function cargo_fmt.extract_top_cargo_row(cargo_results, fields)
    local cargo_args = {}

    if not fields and cargo_results[1] then
        fields = ""
        for key in pairs(cargo_results[1]) do
            fields = fields .. key .. ", "
        end
        fields = fields:sub(1, -3)
    end

    if cargo_results[1] then
        for field in fields:gmatch("[^,]+") do
            local trimmed = field:match("^%s*(.-)%s*$")
            cargo_args[trimmed] = cargo_results[1][trimmed]
        end
    end

    return cargo_args
end


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