Jump to content

Module:TemplateStarter

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

--[[
* Name: TemplateStarter
* Author: Mark W. Datysgeld
* Description: Generates configurable template structures for new pages using ConfigRepository with support for variants and placeholder replacement
* Notes: Template generation from ConfigRepository configs; support for campaign variants; placeholder replacement in boilerplate text; creator field management; caching of template lists; includes error handling and validation
]]

local p = {}

-- Required modules
local ConfigRepository = require('Module:ConfigRepository')
local ErrorHandling = require('Module:ErrorHandling')

-- Cache for template lists
local templateListCache = nil
local templateListCacheTime = 0
local CACHE_DURATION = 300 -- 5 minutes

-- Generate empty template wikitext from template type
function p.generateTemplate(templateType)
    -- Input validation and sanitization
    if not templateType or templateType == "" then
        return "Error: Template type is required"
    end
    
    -- Sanitize input: remove any potentially harmful characters
    templateType = mw.text.trim(tostring(templateType))
    
    if not templateType:match("^[%w_%-]+$") then
        return "Error: Invalid template type format"
    end
    
    -- Create error context
    local errorContext = ErrorHandling.createContext("TemplateStarter")
    
    -- Protected function to get configuration
    local function getTemplateConfig()
        local config = ConfigRepository.getConfig(templateType)
        
        if not config then
            return nil, string.format("Template type '%s' not found in ConfigRepository", templateType)
        end
        
        if not config.fields then
            return nil, string.format("Template type '%s' has no fields configuration", templateType)
        end
        
        return config
    end
    
    -- Get configuration with error protection
    local config = ErrorHandling.protect(errorContext, 'getTemplateConfig', getTemplateConfig, nil)
    if not config then
        return "Error: Failed to load template configuration"
    end
    
    -- Pre-allocate lines table for better performance
    local lines = {}
    local lineCount = 0
    
    -- Check for boilerplate configuration
    local hasBoilerplate = config.boilerplate and (config.boilerplate.intro or config.boilerplate.outro)
    
    -- Add debug comment
    if hasBoilerplate then
        lineCount = lineCount + 1
        lines[lineCount] = "<!-- Boilerplate available for " .. templateType .. " template -->"
    else
        lineCount = lineCount + 1
        lines[lineCount] = "<!-- No boilerplate for this template -->"
    end
    
    -- Start the template
    lineCount = lineCount + 1
    lines[lineCount] = "{{" .. templateType
    
    -- Process each field with error protection
    local function processFields()
        for _, field in ipairs(config.fields) do
            -- Skip hidden fields
            if not field.hidden then
                -- Handle fields with multiple keys (prefer single key, fallback to first of multiple)
                local fieldKey = field.key or (field.keys and field.keys[1])
                
                if fieldKey and fieldKey ~= "" then
                    lineCount = lineCount + 1
                    lines[lineCount] = string.format("|%s = ", fieldKey)
                end
            end
        end
        return true
    end
    
    -- Process fields with error protection
    ErrorHandling.protect(errorContext, 'processFields', processFields, false)
    
    -- Close the template
    lineCount = lineCount + 1
    lines[lineCount] = "}}"
    
    -- Add intro boilerplate after template (if available)
    if config.boilerplate and config.boilerplate.intro then
        lineCount = lineCount + 1
        lines[lineCount] = ""  -- First empty line for separation
        lineCount = lineCount + 1
        lines[lineCount] = ""  -- Second empty line for distance
        lineCount = lineCount + 1
        lines[lineCount] = config.boilerplate.intro
    end

    -- Join with newlines
    return table.concat(lines, "\n")
end

-- Main function to be called from wiki (for testing/preview)
function p.main(frame)
    -- Simple validation without complex error handling for this function
    local args = frame.args
    local parent = frame:getParent()
    local pargs = parent and parent.args or {}
    
    -- Get parameters (check both direct and parent args)
    local articleName = args.articleName or pargs.articleName or args[1] or pargs[1]
    local templateType = args.templateType or pargs.templateType or args[2] or pargs[2]
    
    -- Validate inputs
    if not articleName or mw.text.trim(tostring(articleName)) == "" then
        return "Error: Article name is required"
    end
    
    if not templateType or mw.text.trim(tostring(templateType)) == "" then
        return "Error: Template type is required"
    end
    
    -- Generate the template content
    local content = p.generateTemplate(mw.text.trim(tostring(templateType)))
    
    -- For testing, return the generated content in a pre block
    return string.format('<pre>Page: %s\n\n%s</pre>', 
        mw.text.nowiki(mw.text.trim(tostring(articleName))), 
        mw.text.nowiki(content))
end

-- Generate a dynamic preload template (main function used by JS)
function p.preload(frame)
    local args = frame.args
    local templateType = args.templateType or args[1]
    
    if not templateType or mw.text.trim(tostring(templateType)) == "" then
        return "<!-- No template type specified -->"
    end
    
    templateType = mw.text.trim(tostring(templateType))
    
    -- Parse template to check if it's a variant
    local parsedTemplate = p.parseVariantTemplate(templateType)
    
    if parsedTemplate and parsedTemplate.variant then
        -- Generate combined template for variant
        return p.generateCombinedTemplate(parsedTemplate.baseTemplate, parsedTemplate.variant)
    else
        -- Generate regular template
        return p.generateTemplate(templateType)
    end
end

-- Get list of available templates with caching
function p.getAvailableTemplates()
    local currentTime = os.time()
    
    -- Check if cache is valid
    if templateListCache and (currentTime - templateListCacheTime) < CACHE_DURATION then
        return templateListCache
    end
    
    local templates = {}
    
    if ConfigRepository.templates then
        local index = 1
        for templateName, config in pairs(ConfigRepository.templates) do
            -- Include template if page_creator is not explicitly false
            if templateName and templateName ~= "" and (not config.meta or config.meta.page_creator ~= false) then
                templates[index] = templateName
                index = index + 1
            end
        end
        table.sort(templates)
    end
    
    templateListCache = templates
    templateListCacheTime = currentTime
    
    return templates
end

-- Get list of available templates including campaign combinations
function p.getAvailableTemplatesWithVariants()
    -- Get the filtered list of base templates that are allowed for page creation
    local baseTemplates = p.getAvailableTemplates()
    local templatesWithCampaigns = {}
    
    -- Use a map to ensure uniqueness before adding to the final list
    local templateMap = {}
    
    -- Add base templates to the map
    for _, templateName in ipairs(baseTemplates) do
        templateMap[templateName] = true
    end
    
    -- Iterate through campaigns to find applicable templates
    if ConfigRepository.campaigns then
        for campaignId, campaign in pairs(ConfigRepository.campaigns) do
            if campaign.applicable_templates then
                for _, templateName in ipairs(campaign.applicable_templates) do
                    -- Only include if the base template is allowed for page creation
                    if templateMap[templateName] then
                        local campaignTemplateName = templateName .. " (" .. campaign.name .. ")"
                        templateMap[campaignTemplateName] = true
                    end
                end
            end
        end
    end
    
    -- Convert map keys to a list
    local index = 1
    for templateName, _ in pairs(templateMap) do
        templatesWithCampaigns[index] = templateName
        index = index + 1
    end
    
    -- Sort the final combined list
    table.sort(templatesWithCampaigns)
    
    return templatesWithCampaigns
end

-- Parse campaign template name to get base template and campaign info
function p.parseVariantTemplate(templateName)
    if not templateName or templateName == "" then
        return nil
    end
    
    -- Check if this is a campaign combination by looking for the pattern "Template (Campaign Name)"
    if ConfigRepository.campaigns then
        for campaignId, campaign in pairs(ConfigRepository.campaigns) do
            local expectedName = " (" .. campaign.name .. ")"
            if templateName:sub(-#expectedName) == expectedName then
                local baseTemplateName = templateName:sub(1, -#expectedName - 1)
                -- Verify this is a valid combination
                if campaign.applicable_templates then
                    for _, applicableTemplate in ipairs(campaign.applicable_templates) do
                        if applicableTemplate == baseTemplateName then
                            return {
                                baseTemplate = baseTemplateName,
                                variantKey = campaignId,
                                variant = {
                                    name = templateName,
                                    campaign_template = campaign.json_config
                                }
                            }
                        end
                    end
                end
            end
        end
    end
    
    -- Not a campaign combination, return as base template
    return {
        baseTemplate = templateName,
        variantKey = nil,
        variant = nil
    }
end

-- Generate combined template for variants
function p.generateCombinedTemplate(baseTemplate, variant)
    if not baseTemplate or not variant then
        return "Error: Invalid template combination"
    end
    
    -- Generate base template
    local baseContent = p.generateTemplate(baseTemplate)
    if not baseContent or baseContent:match("^Error:") then
        return baseContent or "Error: Failed to generate base template"
    end
    
    -- Generate campaign template if specified
    local campaignContent = ""
    if variant.campaign_template then
        campaignContent = string.format("{{#invoke:T-Campaign|render|campaign_name=%s}}", variant.campaign_template)
    end
    
    -- Combine templates with blank line separator
    local combinedLines = {}
    local lineCount = 0
    
    -- Add base template
    lineCount = lineCount + 1
    combinedLines[lineCount] = baseContent
    
    -- Add separator
    lineCount = lineCount + 1
    combinedLines[lineCount] = ""
    
    -- Add campaign template
    if campaignContent ~= "" then
        lineCount = lineCount + 1
        combinedLines[lineCount] = campaignContent
    end
    
    return table.concat(combinedLines, "\n")
end

-- Test function to list available templates (clean output for JS consumption)
function p.listTemplates(frame)
    local templates = p.getAvailableTemplatesWithVariants()
    if #templates == 0 then
        return "No templates available"
    end
    return table.concat(templates, ", ")
end

-- Get creator fields for a specific template type
function p.getCreatorFields(templateType)
    if not templateType or templateType == "" then
        return {}
    end
    
    -- Parse template to resolve variants to base template
    local parsedTemplate = p.parseVariantTemplate(templateType)
    local baseTemplateName = parsedTemplate and parsedTemplate.baseTemplate or templateType
    
    local config = ConfigRepository.getConfig(baseTemplateName)
    return config.creatorFields or {}
end

-- Get field definitions for creator fields
function p.getCreatorFieldDefinitions(templateType)
    if not templateType or templateType == "" then
        return {}
    end
    
    local creatorFields = p.getCreatorFields(templateType)
    local fieldDefinitions = {}
    
    for _, fieldKey in ipairs(creatorFields) do
        local fieldDef = ConfigRepository.pageCreatorFields[fieldKey]
        if fieldDef then
            fieldDefinitions[fieldKey] = fieldDef
        end
    end
    
    return fieldDefinitions
end

-- Replace placeholders in boilerplate text with user values
function p.replacePlaceholders(text, values)
    if not text or not values then
        return text
    end
    
    local result = text
    for key, value in pairs(values) do
        if value and value ~= "" then
            result = result:gsub("%$" .. key .. "%$", value)
        end
    end
    
    return result
end

-- Remove empty placeholders from text (for cases where no values are provided)
function p.removeEmptyPlaceholders(text)
    if not text then
        return text
    end
    
    -- Remove any remaining $VARIABLE$ placeholders that weren't filled
    local result = text:gsub("%$[A-Z_]+%$", "")
    
    -- Clean up any resulting double spaces or awkward punctuation
    result = result:gsub("%s+", " ")  -- Multiple spaces to single space
    result = result:gsub("^%s+", "")  -- Leading whitespace
    result = result:gsub("%s+$", "")  -- Trailing whitespace
    result = result:gsub("%s+%.", ".")  -- Space before period
    result = result:gsub("is a%s+based", "is based")  -- Fix "is a  based" to "is based"
    result = result:gsub("is a%s+in", "is in")  -- Fix "is a  in" to "is in"
    
    return result
end

-- Generate template with placeholder replacement
function p.generateTemplateWithValues(templateType, values)
    if not templateType or templateType == "" then
        return "Error: Template type is required"
    end
    
    -- Get base template
    local baseTemplate = p.generateTemplate(templateType)
    
    -- Get boilerplate and replace placeholders
    local config = ConfigRepository.getConfig(templateType)
    if config.boilerplate and config.boilerplate.intro then
        local processedIntro = p.replacePlaceholders(config.boilerplate.intro, values)
        -- Replace the boilerplate in the template
        baseTemplate = baseTemplate:gsub(config.boilerplate.intro, processedIntro)
    end
    
    return baseTemplate
end

-- Get creator field definitions as JSON for JavaScript consumption
function p.getCreatorFieldDefinitionsJSON(frame)
    local args = frame.args
    local templateType = args.templateType or args[1]
    
    if not templateType or templateType == "" then
        return "{}"
    end
    
    local fieldDefinitions = p.getCreatorFieldDefinitions(templateType)
    
    -- Convert to JSON-like string manually (simple approach)
    local jsonParts = {}
    for fieldKey, fieldDef in pairs(fieldDefinitions) do
        local fieldJson = string.format(
            '"%s":{"key":"%s","label":"%s","placeholder":"%s","required":%s}',
            fieldKey,
            fieldDef.key or "",
            fieldDef.label or "",
            fieldDef.placeholder or "",
            fieldDef.required and "true" or "false"
        )
        table.insert(jsonParts, fieldJson)
    end
    
    return "{" .. table.concat(jsonParts, ",") .. "}"
end

return p