Module:T-Campaign: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(201 intermediate revisions by the same user not shown)
Line 1: Line 1:
--[[
-- Module:T-Campaign.lua
* T-Campaign.lua
-- Generic campaign template that dynamically loads campaign data from JSON files
* Generic campaign template that dynamically loads campaign data from JSON files. This template provides a flexible, data-driven approach to campaign templates without cluttering ConfigRepository. It dynamically generates field definitions based on the JSON structure and renders any campaign content.
-- Usage: {{#invoke:T-Campaign|render|campaign_name=NAME}}
*
* Usage: {{#invoke:T-Campaign|render|campaign_name=ASP2025}}
]]


local p = {}
local p = {}
Line 12: Line 9:
local ErrorHandling = require('Module:ErrorHandling')
local ErrorHandling = require('Module:ErrorHandling')
local DatasetLoader = require('Module:DatasetLoader')
local DatasetLoader = require('Module:DatasetLoader')
local WikitextProcessor = require('Module:WikitextProcessor')
local TemplateHelpers = require('Module:TemplateHelpers')
local TemplateStructure = require('Module:TemplateStructure')


-- Register the Campaign template with Blueprint
-- Register the Campaign template with Blueprint
local template = Blueprint.registerTemplate('Campaign', {
local template = Blueprint.registerTemplate('Campaign', {
     features = {  
     features = {  
         title = true,
         fullPage = true, -- Full-page layout
        fields = true,  
         categories = true,
         categories = true,
         errorReporting = true
         errorReporting = true
Line 23: Line 22:
})
})


-- Dynamic configuration - will be populated based on JSON structure
-- Dynamic configuration (from JSON)
template.config = {
template.config = {
     fields = {}, -- Will be dynamically generated
     fields = {},
     categories = {
     categories = { base = {} },
        base = {} -- Will be populated dynamically based on campaign
    },
     constants = {
     constants = {
         title = "Campaign Information" -- Default title, can be overridden by JSON
         title = "Campaign Information",
        tableClass = ""
     }
     }
}
Blueprint.initializeConfig(template)
template.config.blockSequence = {
    'campaignBanner',
    'campaignTitle',
    'campaignInstructions',
    'campaignContent',
    'categories',
    'errors'
}
template.config.blocks = template.config.blocks or {}
-- Helper function: Check for "not applicable" values
local function isNotApplicable(value)
    if not value or type(value) ~= "string" then
        return false
    end
    local lowerVal = value:lower():match("^%s*(.-)%s*$")
    return lowerVal == "n/a" or lowerVal == "na" or lowerVal == "none" or lowerVal == "no" or lowerVal == "false"
end
-- Helper function: Tokenize semicolon-separated strings for instructions
local function tokenizeForInstructions(value)
    if not value or value == "" then
        return value
    end
   
    local items = {}
    for item in value:gmatch("[^;]+") do
        local trimmed = item:match("^%s*(.-)%s*$") -- trim whitespace
        if trimmed and trimmed ~= "" then
            table.insert(items, '<span class="campaign-instruction-token">' .. trimmed .. '</span>')
        end
    end
   
    if #items > 0 then
        return table.concat(items, ' ')
    else
        return value
    end
end
-- Helper function: Generate usage instructions
local function generateUsageInstructions(campaignName, campaignData)
    local output = {}
   
    -- Generate template syntax with all available parameters in user-friendly format
    local templateCall = "{{#invoke:T-Campaign|render|\ncampaign_name=" .. tostring(campaignName) .. "|\n"
   
    -- Add each field as a parameter option with example values (except campaign_intro which is fixed)
    if campaignData.field_definitions then
        for _, fieldDef in ipairs(campaignData.field_definitions) do
            if fieldDef.key ~= "campaign_intro" then
                local exampleValue = "text"
                if fieldDef.type == "list" then
                    exampleValue = "text 1; text 2; etc. (semicolon-separated)"
                end
                templateCall = templateCall .. tostring(fieldDef.key) .. " = " .. exampleValue .. "|\n"
            end
        end
    end
    templateCall = templateCall .. "}}"
   
    -- Use JSON config or fallback to default instruction text
    local headerText = (campaignData.instructions and campaignData.instructions.header_text)
        or "'''READ CAREFULLY'''. These are temporary instructions that will appear only until all parameters outlined here have been filled with data or have 'N/A' in them if the field is not applicable. Choose 'Edit source' at any time and edit the code below if it already exists. It can also be added manually to any page:"
   
    local parameterIntro = (campaignData.instructions and campaignData.instructions.parameter_intro)
        or "'''Available Parameters:'''"
   
    -- Build instruction content with configurable text
    table.insert(output, headerText)
    table.insert(output, "")
    table.insert(output, "<pre>" .. templateCall .. "</pre>")
    table.insert(output, "")
    table.insert(output, parameterIntro)
   
    if campaignData.field_definitions then
        for _, fieldDef in ipairs(campaignData.field_definitions) do
            -- Skip campaign_intro since it's a fixed field from JSON defaults
            if fieldDef.key ~= "campaign_intro" then
                local paramDesc = "* '''<span style=\"color: var(--colored-text);\">" .. tostring(fieldDef.key) .. "</span>''' (" .. tostring(fieldDef.label) .. ", " .. tostring(fieldDef.type) .. "): "
               
                -- Add default value as example if available, with tokenization for lists
                local defaultValue = campaignData.defaults[fieldDef.key]
                if defaultValue and defaultValue ~= "" then
                    if fieldDef.type == "list" then
                        paramDesc = paramDesc .. tokenizeForInstructions(tostring(defaultValue))
                    else
                        paramDesc = paramDesc .. tostring(defaultValue)
                    end
                end
               
                -- Add helpful note for list fields
                if fieldDef.type == "list" then
                    paramDesc = paramDesc .. " (separate multiple values with semicolons)"
                end
                table.insert(output, paramDesc)
            end
        end
    end
    return table.concat(output, "\n")
end
-- Helper function: Get campaign name with simplified fallback logic
local function getCampaignName(args, campaignData)
    -- Primary: standard parameter name
    local campaignName = args.campaign_name
   
    -- Fallback: campaign data template_id or default
    if not campaignName then
        campaignName = (campaignData and campaignData.template_id) or "CAMPAIGN_NAME"
    end
   
    return campaignName
end
-- Helper function: Get recursion depth from frame arguments
local function getRecursionDepth(frame)
    local frameArgs = frame.args or {}
    local parentArgs = (frame:getParent() and frame:getParent().args) or {}
   
    return tonumber(frameArgs._recursion_depth or parentArgs._recursion_depth) or 0
end
-- Campaign Banner
template.config.blocks.campaignBanner = {
    feature = 'fullPage',
    render = function(template, args)
        local context = template._errorContext
       
        if not args._campaign_data or not args._campaign_data.banner then
            ErrorHandling.addStatus(context, 'campaignBanner', 'No banner data found', 'Campaign data or banner config missing')
            return ErrorHandling.formatCombinedOutput(context)
        end
       
        local banner = args._campaign_data.banner
        local bannerContent = banner.content or ""
        local titleText = template.config.constants.title or "Campaign"
       
        -- Combine generic notice-box class with specific campaign class
        local cssClass = "notice-box"
        if banner.css_class and banner.css_class ~= "" then
            cssClass = cssClass .. " " .. banner.css_class
        end
       
        if bannerContent == "" then
            ErrorHandling.addStatus(context, 'campaignBanner', 'Empty banner content', 'No content to display')
            return ErrorHandling.formatCombinedOutput(context)
        end
       
        -- Use the centralized NoticeFactory to create the notice
        local noticeOptions = {
            type = "campaign-js",
            position = "top",
            content = bannerContent,
            title = titleText,
            cssClass = cssClass
        }
       
        return WikitextProcessor.createNoticeForJS(noticeOptions) .. ErrorHandling.formatCombinedOutput(context)
    end
}
-- Campaign Title
template.config.blocks.campaignTitle = {
    feature = 'fullPage',
    render = function(template, args)
        local titleText = template.config.constants.title or "Campaign Information"
        return "== " .. titleText .. " =="
    end
}
-- Campaign Instructions
template.config.blocks.campaignInstructions = {
    feature = 'fullPage',
    render = function(template, args)
        if not args._show_instructions or not args._campaign_data then
            return ""
        end
       
        local campaignName = getCampaignName(args, args._campaign_data)
        local instructions = generateUsageInstructions(campaignName, args._campaign_data)
       
        return '<div class="campaign-instructions">\n== ⚠️ Editing Instructions ==\n\n' .. instructions .. '\n</div>'
    end
}
-- Campaign Content
template.config.blocks.campaignContent = {
    feature = 'fullPage',
    render = function(template, args)
        local output = {}
       
        for _, field in ipairs(template.config.fields or {}) do
            if field.key ~= "usageInstructions" then
                local rawValue = args[field.key]
                local processor = template._processors[field.key] or template._processors.default
               
                if processor then
                    local value = processor(rawValue, args, template, field.type)
                    if value and value ~= "" then
                        if field.key == "campaign_intro" then
                            table.insert(output, value)
                            table.insert(output, "")
                        else
                            table.insert(output, "=== " .. field.label .. " ===")
                            table.insert(output, value)
                            table.insert(output, "")
                        end
                    end
                end
            end
        end
       
        return table.concat(output, "\n")
    end
}
}


-- Generic field processor that handles different data types
-- Generic field processor that handles different data types
local function processFieldValue(value)
local function processFieldValue(value, fieldType)
    if isNotApplicable(value) then
        return nil
    end
 
     if type(value) == "table" then
     if type(value) == "table" then
         if #value > 0 then
         if #value > 0 then
Line 53: Line 276:
             end
             end
             return table.concat(output, "\n")
             return table.concat(output, "\n")
        end
    elseif fieldType == "list" and type(value) == "string" then
        -- Handle semicolon-separated lists with token styling
        local items = {}
        for item in value:gmatch("[^;]+") do
            local trimmed = item:match("^%s*(.-)%s*$") -- trim whitespace
            if trimmed and trimmed ~= "" and not isNotApplicable(trimmed) then
                table.insert(items, '<span class="campaign-token">' .. trimmed .. '</span>')
            end
        end
        if #items > 0 then
            return table.concat(items, ' ')
        else
            return nil -- Return nil if all items were "not applicable"
         end
         end
     else
     else
Line 61: Line 298:
-- Custom preprocessor to load campaign data and generate fields dynamically
-- Custom preprocessor to load campaign data and generate fields dynamically
Blueprint.addPreprocessor(template, function(template, args)
Blueprint.addPreprocessor(template, function(template, args)
     -- Get campaign_name from args (which should already be merged by Blueprint)
     -- CRITICAL: Get campaign_name from args (merged by Blueprint) or current frame for direct module calls
    -- But also check the current frame for direct module invocations
     local campaignName = args.campaign_name
     local campaignName = args.campaign_name
      
      
     -- If not found in args, try to get it from the current frame
     -- Fallback for direct module invocations where args may not be merged yet
     if (not campaignName or campaignName == "") and template.current_frame then
     if (not campaignName or campaignName == "") and template.current_frame then
         local frameArgs = template.current_frame.args or {}
         local frameArgs = template.current_frame.args or {}
Line 72: Line 308:
      
      
     if not campaignName or campaignName == "" then
     if not campaignName or campaignName == "" then
        ErrorHandling.addError(
            template._errorContext or ErrorHandling.createContext('T-Campaign'),
            'preprocessor',
            'No campaign_name provided',
            nil,
            false
        )
         return args
         return args
     end
     end
      
      
     -- Load campaign data from JSON
     -- Load campaign data from JSON
     local campaignData = DatasetLoader.load('Data:Campaigns/' .. campaignName .. '.json')
     local campaignData = DatasetLoader.get('Campaigns/' .. campaignName)
     if not campaignData then
     if not campaignData or not campaignData.defaults or not campaignData.field_definitions then
        ErrorHandling.addError(
            template._errorContext or ErrorHandling.createContext('T-Campaign'),
            'preprocessor',
            'Failed to load campaign data for: ' .. campaignName,
            nil,
            false
        )
         return args
         return args
     end
     end
      
      
     -- Dynamically generate field definitions based on JSON structure
     -- Simple console message
    ErrorHandling.addStatus(
        template._errorContext,
        'campaignLoader',
        'Campaign loaded successfully for ' .. campaignName,
        nil
    )
   
    -- CRITICAL: Detect custom parameters BEFORE merging defaults to determine template mode
    local customParameters = {}
    local hasCustomParameters = false
    for k, v in pairs(args) do
        -- Skip campaign_name, internal parameters, and empty values
        if k ~= "campaign_name" and
          not k:match("^_") and  -- Skip internal parameters like _recursion_depth
          v and v ~= "" then
            customParameters[k] = true
            hasCustomParameters = true
        end
    end
   
    -- Check if ALL fields are provided as custom parameters (excluding campaign_intro which is fixed)
    local hasAllParameters = true
    for _, fieldDef in ipairs(campaignData.field_definitions) do
        if fieldDef.key ~= "campaign_intro" and not customParameters[fieldDef.key] then
            hasAllParameters = false
            break
        end
    end
   
    -- Determine template mode based on parameter completeness
    local templateMode
    if not hasCustomParameters then
        templateMode = "documentation"
    elseif hasAllParameters then
        templateMode = "complete"
    else
        templateMode = "partial"
    end
   
    -- Store mode state for rendering
    args._template_mode = templateMode
    args._show_instructions = (templateMode ~= "complete")
    args._campaign_data = campaignData
   
    -- Defaults are only used for parameter documentation, not content rendering
   
    -- Generate field definitions based on mode
     local fields = {}
     local fields = {}
     for key, value in pairs(campaignData) do
   
         -- Skip campaign_name as it's a control parameter
    -- Always show campaign content fields (they'll show placeholder text when empty)
         if key ~= "campaign_name" then
     for _, fieldDef in ipairs(campaignData.field_definitions) do
            -- Create human-readable labels from JSON keys
         -- CRITICAL: Skip 'title' as it is not a content field
            local label = key:gsub("_", " "):gsub("^%l", string.upper)
         if fieldDef.key ~= "title" then
             table.insert(fields, {key = key, label = label})
             table.insert(fields, {
                key = fieldDef.key,
                label = fieldDef.label,
                type = fieldDef.type
            })
         end
         end
     end
     end
      
      
     -- Sort fields to ensure consistent ordering
     -- Add usage instructions in documentation and partial modes
     table.sort(fields, function(a, b) return a.key < b.key end)
     if templateMode ~= "complete" then
        table.insert(fields, {
            key = "usageInstructions",
            label = "Usage Instructions",
            type = "text"
        })
        args.usageInstructions = "documentation"
    end
      
      
     template.config.fields = fields
     template.config.fields = fields
      
      
     -- Override title if provided in JSON
     -- Override title if provided in JSON defaults
     if campaignData.title then
     if campaignData.defaults.title then
         template.config.constants.title = campaignData.title
         template.config.constants.title = campaignData.defaults.title
     end
     end
      
      
     -- Merge campaign data into args for rendering
     -- Add campaign-specific category, defaulting to template_id
     for k, v in pairs(campaignData) do
     local category_value = campaignData.category or campaignData.template_id
        args[k] = v
     template.config.categories.base = {category_value}
    end
   
    -- Add campaign-specific category
     template.config.categories.base = {campaignName}
      
      
     return args
     return args
Line 133: Line 409:
end
end


-- Set up a universal field processor that will be used for all fields
-- Set up a universal field processor that handles all field types
-- This processor will be called by the Blueprint system for each field
template._processors.default = function(value, args, template, fieldType)
template._processors.default = processFieldValue
    if value and value ~= "" then
        return processFieldValue(value, fieldType or "text")
    else
        -- Show placeholder for empty fields in documentation and partial modes
        if args._template_mode ~= "complete" then
            return "''Please see usage instructions above to customize this field.''"
        end
        return nil -- Don't display empty fields in complete mode
    end
end
 
-- SPECIAL: campaign_intro processor - always uses JSON default, never user input
template._processors.campaign_intro = function(value, args, template, fieldType)
    -- CRITICAL: Always use campaign data default, ignore user input to maintain consistency
    if args._campaign_data and args._campaign_data.defaults and args._campaign_data.defaults.campaign_intro then
        local defaultIntro = args._campaign_data.defaults.campaign_intro
        return "''" .. tostring(defaultIntro) .. "''"
    else
        -- Fallback if no campaign data available
        if args._template_mode ~= "complete" then
            return "''Campaign introduction will appear here from JSON defaults.''"
        end
        return nil
    end
end


-- Also set up specific processors for common field patterns
-- Special processor for usage instructions
template._processors.title = function(value) return tostring(value) end
template._processors.usageInstructions = function(value, args, template)
template._processors.regions_covered = processFieldValue
     if not args._show_instructions or not args._campaign_data then
template._processors.services_provided = processFieldValue
         return nil -- Only render when instructions should be shown
template._processors.areas_of_expertise = processFieldValue
template._processors.icann_experience = function(value)  
     if value and value ~= "" then
         return tostring(value)
     end
     end
     return nil -- Don't display empty fields
   
    local campaignName = getCampaignName(args, args._campaign_data)
    local instructions = generateUsageInstructions(campaignName, args._campaign_data)
   
     return "\n----\n'''Usage Instructions'''\n\n" .. instructions
end
end


-- Export the render function
function p.render(frame)
function p.render(frame)
     return template.render(frame)
     template.current_frame = frame
   
    local depth = getRecursionDepth(frame)
   
    if depth > 3 then
        return '<span class="error">Template recursion depth exceeded (limit: 3)</span>'
    end
   
    if not template._errorContext then
        template._errorContext = require('Module:ErrorHandling').createContext(template.type)
    end
   
    if not template.config.meta then
        require('Module:LuaTemplateBlueprint').initializeConfig(template)
    end
   
    -- Merge arguments: frame args override parent args
    local args = {}
    local parentArgs = frame:getParent().args or {}
    local frameArgs = frame.args or {}
   
    for k, v in pairs(parentArgs) do args[k] = v end
    for k, v in pairs(frameArgs) do args[k] = v end
   
    args = TemplateHelpers.normalizeArgumentCase(args)
   
    args._recursion_depth = tostring(depth + 1)
   
    args = Blueprint.runPreprocessors(template, args)
   
    local structureConfig = {
        tableClass = (template.config.constants and template.config.constants.tableClass) or "template-table",
        blocks = {},
        containerTag = template.features.fullPage and "div" or "table"
    }
   
    local renderingSequence = Blueprint.buildRenderingSequence(template)
   
    if renderingSequence._length == 0 then
        return ""
    end
   
    for i = 1, renderingSequence._length do
        table.insert(structureConfig.blocks, function(a)
            return renderingSequence[i](a)
        end)
    end
   
    local result = TemplateStructure.render(args, structureConfig, template._errorContext)
   
    template.current_frame = nil
   
    return result
end
end


return p
return p