Module:Cargo format utilities
Appearance
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