Jump to content

Module:Table functions

From WhiskerWiki

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
Cookies help us deliver our services. By using our services, you agree to our use of cookies.