Module:Table functions
Appearance
Documentation for this module may be created at Module:Table functions/doc
-- ================================================================================
-- Module dependencies
-- ================================================================================
local getArgs = require('Module:Arguments').getArgs
local cargo = mw.ext.cargo -- for cargo queries if needed
local cf = require("Module:Custom_functions")
local ibf = require("Module:Infobox_functions")
local html = mw.html.create()
local tf = {}
-- ================================================================================
-- Helper functions inolved in query Cargo
-- ================================================================================
function tf.build_where_clause(cargo_focal_field_type, cargo_focal_field, filter_values_list, not_values_list)
-- Set default value for not_values_list if it's not provided
not_values_list = not_values_list or {}
-- Ensure cargo_focal_field is not nil
if cargo_focal_field == nil then
error("cargo_focal_field cannot be nil")
end
-- Ensure filter_values_list is not nil
if filter_values_list == nil then
error("filter_values_list cannot be nil")
end
-- Build Cargo where statement based on field type (i.e. "HOLDS" vs. "=" depending on if field is list type or not)
local where_clauses = {}
if string.find(cargo_focal_field_type, "list") ~= nil then
for _, value in ipairs(filter_values_list) do
if value == nil then
error("Value in filter_values_list cannot be nil")
end
table.insert(where_clauses, string.format("%s HOLDS '%s'", cargo_focal_field, value))
end
else
for _, value in ipairs(filter_values_list) do
if value == nil then
error("Value in filter_values_list cannot be nil")
end
table.insert(where_clauses, string.format("%s = '%s'", cargo_focal_field, value))
end
end
-- Build NOT clause if not_filter_values are provided
local not_clauses = {}
if #not_values_list > 0 then
if string.find(cargo_focal_field_type, "list") ~= nil then
for _, value in ipairs(not_values_list) do
if value == nil then
error("Value in not_values_list cannot be nil")
end
table.insert(not_clauses, string.format("%s HOLDS '%s'", cargo_focal_field, value))
end
else
for _, value in ipairs(not_values_list) do
if value == nil then
error("Value in not_values_list cannot be nil")
end
table.insert(not_clauses, string.format("%s != '%s'", cargo_focal_field, value))
end
end
end
-- Join the where clauses with OR
local where_clause = table.concat(where_clauses, " OR ")
-- Join the not clauses with AND NOT if there are any not clauses
if #not_clauses > 0 then
local not_clause = table.concat(not_clauses, " AND NOT ")
where_clause = string.format("(%s) AND NOT (%s)", where_clause, not_clause)
end
return where_clause
end
--[[ This function takes the name of a Cargo template (which presumably but not critically has a companion Cargo table with the same name), and returns a flat, 1D array that
represents the field types for that Cargo table (as defined by the template). Here is a sample structure:
{ organization = "page",
image = "file",
non_english_name = "string",
parent_organization = "list of page",
website = "url"
}
]]--
function tf.get_cargo_table_schema(cargo_template_name)
-- Initialize a debug message string
local debug_messages = "Debug: Entered get_cargo_table_schema function.\n"
-- Ensure the cargo_template_name is provided
if not cargo_template_name or cargo_template_name == "" then
debug_messages = debug_messages .. "Error: Template name is required.\n"
return "Template name is required.", debug_messages
end
debug_messages = debug_messages .. "Template name: " .. cargo_template_name .. "\n"
local template_title = mw.title.new(cargo_template_name, 'Template')
if not template_title then
debug_messages = debug_messages .. "Error: Failed to create title object for template.\n"
return "Failed to create title object for template.", debug_messages
end
debug_messages = debug_messages .. "Template title object created: " .. tostring(template_title) .. "\n"
local template_content = template_title:getContent()
if not template_content then
debug_messages = debug_messages .. "Error: Failed to retrieve template content for template '" .. cargo_template_name .. "'.\n"
return "Failed to retrieve template content.", debug_messages
end
debug_messages = debug_messages .. "Template content retrieved successfully.\n"
-- Use a generic pattern to match any table name in the cargo_declare block
local pattern = '{{#cargo_declare:_table=[^|}]+(.-)}}'
local schema_string = template_content:match(pattern)
if not schema_string then
debug_messages = debug_messages .. "Error: Failed to find cargo_declare in template content.\n"
return "Failed to find cargo_declare in template.", debug_messages
end
debug_messages = debug_messages .. "Cargo declare block found.\n"
-- Parse the schema definition to extract fields and types
local schema = {}
for field_name, field_type in schema_string:gmatch('|([^|=]+)%s*=%s*([^\n|]+)') do
field_type = field_type:gsub('List %b() of ', 'list_of_')
field_type = field_type:gsub('list_of_(%w+)', function(x) return 'list_of_' .. string.lower(x) end)
field_type = field_type:gsub('[ ,()]+', '_')
field_type = mw.text.trim(field_type)
field_type = string.lower(field_type)
schema[mw.text.trim(field_name)] = field_type
end
-- Convert the schema table to a string format for debugging
local schema_str = "{\n"
for field_name, field_type in pairs(schema) do
schema_str = schema_str .. " " .. field_name .. " = \"" .. field_type .. "\",\n"
end
schema_str = schema_str .. "}"
-- Add the schema to the debug messages
debug_messages = debug_messages .. "Schema: " .. schema_str .. "\n"
-- Return the schema and debug messages
return schema, debug_messages
end
--[[This function takes a standard nested array from a cargo query as well as an array representing
the desired format types for each field and outputs a formatted array of the same structure. An example
field_formats array would look like this with the following four valid format arguments:
local field_formats = {
Organization = "page",
Acronym = "string",
Image="file"
Link = "url",
Parent_organization = "page",
Type = "string",
Focus = "list_of_string",
Focus_keywords = "list_of_string",
Species_purview = "list_of_page",
}
]]--
local function format_cargo_results(cargo_results, field_formats, field_name_mappings)
local formatted_results = {}
field_name_mappings = field_name_mappings or {} -- Fallback to empty table if no mappings provided
for _, result in ipairs(cargo_results) do
local formatted_result = {}
for field, value in pairs(result) do
if value and value ~= "" then -- Check if the value exists and is not an empty string
local format = field_formats[field]
local formatted_value = value
if format == "list_of_string" then
-- Split the value by commas and trim spaces, then rejoin using comma
local string_list = mw.text.split(value, ",")
for i, v in ipairs(string_list) do
string_list[i] = mw.text.trim(v)
end
formatted_value = table.concat(string_list, ", ")
elseif format == "page" then
formatted_value = "[[" .. value .. "]]"
elseif format == "list_of_page" then
local page_links = {}
local values = mw.text.split(value, ",")
local current_value = ""
for i, v in ipairs(values) do
v = mw.text.trim(v)
if current_value ~= "" then
current_value = current_value .. "," .. v
else
current_value = v
end
-- If the current value contains multiple words, treat it as a complete entry
if not v:match("^%s*$") then
table.insert(page_links, "[[" .. current_value .. "]]")
current_value = ""
end
end
-- Handle any remaining value that wasn't added
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
formatted_value = string.format('[[File:%s|thumb|100px]]', value)
end
-- Apply new field name if mapping exists
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
tf.format_cargo_results = format_cargo_results
--[[This function takes cargo results array and formats it as a wiki table that can be collapsed. The 'fields' parameter
can be used to specify the cargo fields to display and the collapse and character_cutoff arguments control table and cell collapsing.
The character_cutoff parameter excludes fields where an external url is detected as this was causing formatting problems and these fields
are collapsed into their link text anyway]]--
function tf.generate_wiki_table(cargo_results, header, fields, collapse, character_cutoff)
if type(fields) ~= "string" then
error("fields must be a comma-separated string")
end
character_cutoff = character_cutoff or 100 -- Default character limit
-- 1. Extract field names
local clean_fields = {}
for field in fields:gmatch("[^,]+") do
table.insert(clean_fields, field:match("[^ ]+"))
end
-- 2. Begin table output
local output = '{| class="wikitable-cargo-table sortable"\n'
-- 3. Generate headers
for _, field in ipairs(clean_fields) do
output = output .. '! ' .. field:gsub("_", " ") .. '\n'
end
-- Generate table rows and cell values
for _, row in ipairs(cargo_results) do
output = output .. '|-\n'
for _, field in ipairs(clean_fields) do
local value = row[field] or ""
-- Check if the value contains an external link
local contains_external_link = value:match("%[https?://.-%]")
-- Apply collapsible content logic, but skip if contains an external link
if string.len(value) > character_cutoff and not contains_external_link then
-- Check if the value is a wiki link
local is_wiki_link = value:match("^%[%[.-%]%]$")
-- Use appropriate formatting
if is_wiki_link then
-- Keep wiki links as is for correct rendering
value = string.format('<div class="mw-collapsible mw-collapsed">%s</div>', value)
else
-- Use nowiki for non-link content
value = string.format('<div class="mw-collapsible mw-collapsed">%s</div>', mw.text.nowiki(value))
end
end
output = output .. '| ' .. value .. '\n'
end
end
output = output .. '|}\n'
-- 5. Add overall collapsible functionality if needed
if collapse then
output = '<div class="mw-collapsible mw-collapsed">\n' .. output .. '</div>\n'
end
-- Add clear fix after collapsible table
output = output .. '<div style="clear: both;"></div>\n'
return header .. output
end
function tf.extract_non_blank_fields(cargo_results, ranks, count)
local valid_rank_fields = {}
for _, rank_field in ipairs(ranks) do
local is_valid = false
-- Iterate through each "row" in cargo_results
for _, row in ipairs(cargo_results) do
local rank_value = row[rank_field]
-- Check if the field is not nil, not an empty string, and not an empty link
if rank_value and rank_value ~= "" and not rank_value:match("^%[%[%s*%]%]$") then
is_valid = true
break
end
end
if is_valid then
table.insert(valid_rank_fields, rank_field)
end
-- Break if we've reached the count limit (if applicable)
if count and #valid_rank_fields == count then
break
end
end
return valid_rank_fields
end
--[[ This simple function checks whether a string is anywhere in a Cargo table and returns true or false.
I initially used it to check if a function that is normally calls from a species page is called from a different page. In this way I can check if
The title of the wiki page is actually a taxonomic name or not quickly --]]
function tf.is_value_in_cargo_results(cargo_results, search_value)
for _, item in ipairs(cargo_results) do
for _, value in pairs(item) do
if value == search_value then
return true
end
end
end
return false
end
-- the main functions
function tf.get_cargo_results(frame)
local args = getArgs(frame)
local collapse_cutoff = args.collapse_cutoff
local header = ""
local output = ""
local output_len = ""
local tables = args.tables
local fields = args.fields
local cargo_args = args.cargo_args
local cargo_results = cargo.query(tables, fields, cargo_args)
-- format cargo information in place
local field_formats = {
Species = "page",
Number_unicuspids = "string",
Postmandibular_canal = "string",
Unicuspid_notes = "string",
Tines_upper_incisors = "string",
Tine_size = "string",
Tine_position = "string",
Shape_medial_edge_incisors = "string",
Skull_shape = "string",
Dental_characters = "string",
Skull_length
}
local cargo_results_formatted = format_cargo_results(cargo_results, field_formats)
-- control row cutoff for collapsing display
local row_cutoff = tonumber(args.row_cutoff)
local character_cutoff = tonumber(args.character_cutoff)
local collapse = false
if #cargo_results_formatted > row_cutoff then
collapse = true
end
-- select the fields that should be displayed
local display_fields = fields
-- generate the html table
local output
if #cargo_results_formatted > 0 then
local header = ""
output = tf.generate_wiki_table(cargo_results_formatted, header, display_fields, collapse, character_cutoff)
else
output = "No results found on the wiki for this species."
end
return output .. "<br/>"
end
function tf.table_to_wiki_text(t, indent)
indent = indent or ""
if type(t) ~= "table" then
return tostring(t)
end
local lines = {}
for k, v in pairs(t) do
if type(v) == "table" then
table.insert(lines, indent .. tostring(k) .. " = {")
table.insert(lines, tf.table_to_wiki_text(v, indent .. " "))
table.insert(lines, indent .. "}")
else
table.insert(lines, indent .. tostring(k) .. " = " .. tostring(v))
end
end
return table.concat(lines, "\n")
end
-- Create an alias for this function since I have to use it quickly all the time
tf.table_printer = tf.table_to_wiki_text
return tf