Jump to content

Module:ListGeneration

Documentation for this module may be created at Module:ListGeneration/doc

--[[
* Name: ListGeneration
* Author: Mark W. Datysgeld
* Description: Centralized and flexible module for generating various types of lists from delimited strings, designed to replace fragmented list generation logic
* Notes: Handles semicolon-delimited strings automatically; supports multiple list modes (bullet, bullet_custom, invisible, comma); allows custom CSS classes on list container; provides itemHook for custom per-item processing and formatting; accepts both string input (auto-split) and pre-split table input
]]

local p = {}

-- Dependencies
local NormalizationText = require('Module:NormalizationText')

-- Core list creation function
-- @param input string The semicolon-delimited string of list items.
-- @param options table A table of configuration options.
--   - mode (string): 'bullet' (default), 'bullet_custom', 'invisible', 'comma'.
--   - bulletChar (string): The character to use for custom bullets.
--   - listClass (string): A custom CSS class for the <ul> element.
--   - itemHook (function): A function to process each list item.
-- @return string The formatted list as an HTML string or comma-separated text.
function p.createList(input, options)
    if not input or (type(input) == 'string' and input == '') or (type(input) == 'table' and #input == 0) then
        return ''
    end

    -- Initialize options with defaults
    options = options or {}
    local mode = options.mode or 'bullet'
    local bulletChar = options.bulletChar
    local listClass = options.listClass
    local itemHook = options.itemHook

    -- If input is a string, split it. Otherwise, assume it's a pre-split table.
    local items
    if type(input) == 'string' then
        -- Default to splitting only by semicolon. This is a safer default than also splitting by "and", which can break up valid entity names.
        -- Callers that need more complex splitting logic should pre-split the string and pass a table instead.
        local semicolonOnlyPattern = {{pattern = ";%s*", replacement = ";"}}
        items = NormalizationText.splitMultiValueString(input, semicolonOnlyPattern)
    else
        items = input
    end

    if #items == 0 then
        return ''
    end

    -- Process items with the hook if provided, allowing for custom formatting
    if type(itemHook) == 'function' then
        local processedItems = {}
        for i, item in ipairs(items) do
            local processed = itemHook(item)
            if processed then
                table.insert(processedItems, processed)
            end
        end
        items = processedItems
    end

    -- If there's only one item and no special formatting is needed, return it directly without list formatting
    if #items == 1 then
        local singleItem = items[1]
        local hasCustomBullet = (mode == 'bullet_custom' and bulletChar and bulletChar ~= '')
        local hasSpecialClass = (type(singleItem) == 'table' and singleItem.class and singleItem.class ~= '')
        
        -- Only bypass list formatting if there's no custom bullet AND no special class
        if not hasCustomBullet and not hasSpecialClass then
            if type(singleItem) == 'table' then
                return singleItem.content or ''
            else
                return singleItem
            end
        end
        -- If we have custom bullets or special classes, continue to list formatting below
    end

    -- Handle comma-separated mode for simple text lists
    if mode == 'comma' then
        return table.concat(items, ', ')
    end

    -- Handle all HTML list-based modes
    if mode == 'bullet' or mode == 'bullet_custom' or mode == 'invisible' then
        local listItems = {}
        local listStyle = ''

        -- Handle custom bullet character via inline style
        if mode == 'bullet_custom' and bulletChar and bulletChar ~= '' then
            listStyle = string.format(' style="list-style-type: \'%s\';"', bulletChar)
        end

        -- Build the individual list items
        for _, itemData in ipairs(items) do
            local content = ''
            local itemClass = ''
            if type(itemData) == 'table' then
                content = itemData.content or ''
                if itemData.class and itemData.class ~= '' then
                    itemClass = string.format(' class="%s"', itemData.class)
                end
            elseif type(itemData) == 'string' then
                content = itemData
            end

            if content ~= '' then
                table.insert(listItems, string.format('<li%s>%s</li>', itemClass, content))
            end
        end

        -- Build the final CSS class string for the <ul> element
        local finalClass = 'template-list'
        if mode == 'invisible' then
            finalClass = finalClass .. ' list-style-none'
        end
        if listClass and listClass ~= '' then
            finalClass = finalClass .. ' ' .. listClass
        end

        return string.format('<ul class="%s"%s>%s</ul>', finalClass, listStyle, table.concat(listItems, ''))
    end

    -- Fallback for a single item or an unknown mode
    if #items == 1 then
        return items[1]
    end

    -- Default fallback for multiple items with an unknown mode is a comma-separated list
    return table.concat(items, ', ')
end

return p