Module:T-Campaign: Difference between revisions

// via Wikitext Extension for VSCode
Tag: Reverted
// via Wikitext Extension for VSCode
Tags: Manual revert Reverted
Line 1: Line 1:
--[[
-- 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=ASP2025}}
*
* 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
Line 18: Line 18:
         fullPage = true, -- Full-page layout
         fullPage = true, -- Full-page layout
         categories = true,
         categories = true,
         errorReporting = false -- REVIEW
         errorReporting = true
     }
     }
})
})
Line 24: Line 24:
-- Dynamic configuration (from JSON)
-- Dynamic configuration (from JSON)
template.config = {
template.config = {
     fields = {}, -- Dynamically generated
     fields = {},
     categories = {
     categories = { base = {} },
        base = {} -- Populated dynamically based on campaign
    },
     constants = {
     constants = {
         title = "Campaign Information", -- Default title, can be overridden by JSON
         title = "Campaign Information",
         tableClass = "" -- Empty to prevent template box styling
         tableClass = ""
     }
     }
}
}


-- Blueprint default: Initialize standard configuration
Blueprint.initializeConfig(template)
Blueprint.initializeConfig(template)


-- Custom block sequence (bypass standard Blueprint blocks)
template.config.blockSequence = {
template.config.blockSequence = {
     'campaignBanner',
     'campaignBanner',
     'campaignTitle',
     'campaignTitle',  
     'campaignInstructions',
     'campaignInstructions',
     'campaignContent',
     'campaignContent',
Line 47: Line 43:
}
}


-- Initialize custom blocks
template.config.blocks = template.config.blocks or {}
template.config.blocks = template.config.blocks or {}


Line 127: Line 122:
end
end


-- Helper function: Get campaign name with fallback logic (consolidates repeated logic)
-- Helper function: Get campaign name with simplified fallback logic
local function getCampaignName(args, campaignData)
local function getCampaignName(args, campaignData)
    local campaignName = args.campaign_name or args.campaignname or args.Campaign_name or args.CampaignName
     -- Primary: standard parameter name
   
     local campaignName = args.campaign_name
     -- If still nil, try to get it from the campaign data
     if not campaignName and campaignData and campaignData.template_id then
        campaignName = campaignData.template_id
    end
      
      
     -- Final fallback
     -- Fallback: campaign data template_id or default
     if not campaignName then
     if not campaignName then
         campaignName = "CAMPAIGN_NAME"
         campaignName = (campaignData and campaignData.template_id) or "CAMPAIGN_NAME"
     end
     end
      
      
Line 144: Line 135:
end
end


-- Block 1: Campaign Banner (sets noticeHandler config for top-of-page injection)
-- 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 = {
template.config.blocks.campaignBanner = {
     feature = 'fullPage',
     feature = 'fullPage',
     render = function(template, args)
     render = function(template, args)
         local context = template._errorContext or ErrorHandling.createContext('T-Campaign')
         local context = template._errorContext
          
          
         if not args._campaign_data or not args._campaign_data.banner then
         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')
             ErrorHandling.addStatus(context, 'campaignBanner', 'No banner data found', 'Campaign data or banner config missing')
             return ErrorHandling.formatCombinedOutput(context) -- Return debug output
             return ErrorHandling.formatCombinedOutput(context)
         end
         end
          
          
Line 164: Line 163:
         end
         end
          
          
        -- Apply string normalization to ALL content first (critical for wiki link processing)
        bannerContent = bannerContent:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
       
        -- Use TemplateStarter's proven placeholder replacement strategy
         local placeholderValues = {
         local placeholderValues = {
             CAMPAIGN_NAME = args._campaign_data.defaults.title or "Campaign"
             CAMPAIGN_NAME = args._campaign_data.defaults.title or "Campaign"
         }
         }
          
          
         -- Apply TemplateStarter's replacePlaceholders function logic
         bannerContent = WikitextProcessor.processContentForFrontend(bannerContent, placeholderValues, context)
        local function 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
       
        -- Apply placeholder replacement
        bannerContent = replacePlaceholders(bannerContent, placeholderValues)
       
        -- Clean up any remaining unfilled placeholders (TemplateStarter's removeEmptyPlaceholders logic)
        bannerContent = bannerContent:gsub("%$[A-Z_]+%$", "")
        bannerContent = bannerContent:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
       
        -- UNIFIED PIPELINE: Process ALL wiki links with custom patterns (no frame:preprocess)
        local originalContent = bannerContent
       
        -- Pattern 1: [[Page Name]] -> HTML link (simple page links)
        bannerContent = bannerContent:gsub('%[%[([^#|%]]+)%]%]', function(pageName)
            local spacesReplaced = pageName:gsub(' ', '_')
            local success1, pageUrl = pcall(function()
                return tostring(mw.uri.fullUrl(spacesReplaced))
            end)
           
            if not success1 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'mw.uri.fullUrl failed for page link', 'Page: ' .. pageName .. ', Error: ' .. tostring(pageUrl))
                return '[[' .. pageName .. ']]'
            end
           
            local success2, encodedName = pcall(function()
                return mw.text.encode(pageName)
            end)
           
            if not success2 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'mw.text.encode failed for page link', 'Page: ' .. pageName .. ', Error: ' .. tostring(encodedName))
                return '[[' .. pageName .. ']]'
            end
           
            local success3, result = pcall(function()
                return string.format('<a href="%s">%s</a>', pageUrl, encodedName)
            end)
           
            if not success3 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'string.format failed for page link', 'Page: ' .. pageName .. ', Error: ' .. tostring(result))
                return '[[' .. pageName .. ']]'
            end
           
            return result
        end)
       
        -- Pattern 2: [[Page|text]] -> HTML link (page links with custom text)
        bannerContent = bannerContent:gsub('%[%[([^#|%]]+)|([^%]]+)%]%]', function(pageName, text)
            local success1, pageUrl = pcall(function()
                return tostring(mw.uri.fullUrl((pageName:gsub(' ', '_'))))
            end)
           
            if not success1 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 2 mw.uri.fullUrl failed', 'Error: ' .. tostring(pageUrl))
                return '[[' .. pageName .. '|' .. text .. ']]'
            end
           
            local success2, encodedText = pcall(function()
                local encoded = mw.text.encode(text)
                return encoded
            end)
           
            if not success2 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 2 mw.text.encode failed', 'Error: ' .. tostring(encodedText))
                return '[[' .. pageName .. '|' .. text .. ']]'
            end
           
            local success3, result = pcall(function()
                return string.format('<a href="%s">%s</a>', pageUrl, encodedText)
            end)
           
            if not success3 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 2 string.format failed', 'Error: ' .. tostring(result))
                return '[[' .. pageName .. '|' .. text .. ']]'
            end
           
            return result
        end)
          
          
        -- Pattern 3: [[#anchor|text]] -> HTML anchor link
        bannerContent = bannerContent:gsub('%[%[#([^|%]]+)|([^%]]+)%]%]', function(anchor, text)
            local success1, encodedAnchor = pcall(function()
                return mw.uri.anchorEncode(anchor)
            end)
           
            if not success1 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 3 mw.uri.anchorEncode failed', 'Error: ' .. tostring(encodedAnchor))
                return '[[#' .. anchor .. '|' .. text .. ']]'
            end
           
            local success2, encodedText = pcall(function()
                local encoded = mw.text.encode(text)
                return encoded
            end)
           
            if not success2 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 3 mw.text.encode failed', 'Error: ' .. tostring(encodedText))
                return '[[#' .. anchor .. '|' .. text .. ']]'
            end
           
            local success3, result = pcall(function()
                return string.format('<a href="#%s">%s</a>', encodedAnchor, encodedText)
            end)
           
            if not success3 then
                ErrorHandling.addStatus(context, 'campaignBanner', 'Pattern 3 string.format failed', 'Error: ' .. tostring(result))
                return '[[#' .. anchor .. '|' .. text .. ']]'
            end
           
            return result
        end)
       
        -- Set notice data for JavaScript injection
         local noticeData = {
         local noticeData = {
             type = "campaign",
             type = "campaign",
Line 304: Line 176:
         }
         }
          
          
        -- Output data attributes for NoticeHandler to read (instead of mw.config)
         local success, result = pcall(function()
         local success, result = pcall(function()
             return string.format(
             return string.format(
Line 324: Line 195:
}
}


-- Block 2: Campaign Title
-- Campaign Title
template.config.blocks.campaignTitle = {
template.config.blocks.campaignTitle = {
     feature = 'fullPage',
     feature = 'fullPage',
Line 333: Line 204:
}
}


-- Block 2: Campaign Instructions (only in documentation/partial modes)
-- Campaign Instructions
template.config.blocks.campaignInstructions = {
template.config.blocks.campaignInstructions = {
     feature = 'fullPage',
     feature = 'fullPage',
     render = function(template, args)
     render = function(template, args)
         if not args._show_instructions or not args._campaign_data then
         if not args._show_instructions or not args._campaign_data then
             return "" -- Only render when instructions should be shown
             return ""
         end
         end
          
          
Line 348: Line 219:
}
}


-- Block 3: Campaign Content (all campaign fields)
-- Campaign Content
template.config.blocks.campaignContent = {
template.config.blocks.campaignContent = {
     feature = 'fullPage',
     feature = 'fullPage',
Line 355: Line 226:
          
          
         for _, field in ipairs(template.config.fields or {}) do
         for _, field in ipairs(template.config.fields or {}) do
            -- Skip usage instructions field (handled by campaignInstructions block)
             if field.key ~= "usageInstructions" then
             if field.key ~= "usageInstructions" then
                 local rawValue = args[field.key]
                 local rawValue = args[field.key]
Line 361: Line 231:
                  
                  
                 if processor then
                 if processor then
                    -- Pass field type as additional parameter to fix broken detection
                     local value = processor(rawValue, args, template, field.type)
                     local value = processor(rawValue, args, template, field.type)
                     if value and value ~= "" then
                     if value and value ~= "" then
                        -- Special handling for campaign_intro - no section header
                         if field.key == "campaign_intro" then
                         if field.key == "campaign_intro" then
                             table.insert(output, value)
                             table.insert(output, value)
Line 423: Line 291:
-- 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 445: Line 312:
     -- Simple console message
     -- Simple console message
     ErrorHandling.addStatus(
     ErrorHandling.addStatus(
         template._errorContext or ErrorHandling.createContext('T-Campaign'),
         template._errorContext,
         'campaignLoader',
         'campaignLoader',
         'Campaign loaded successfully for ' .. campaignName,
         'Campaign loaded successfully for ' .. campaignName,
Line 451: Line 318:
     )
     )
      
      
     -- Detect what custom parameters are provided BEFORE merging defaults
     -- CRITICAL: Detect custom parameters BEFORE merging defaults to determine template mode
     local customParameters = {}
     local customParameters = {}
     local hasCustomParameters = false
     local hasCustomParameters = false
Line 473: Line 340:
     end
     end
      
      
     -- Determine mode:
     -- Determine template mode based on parameter completeness
     -- Pure Documentation: only campaign_name provided
     local templateMode
     -- Partial Mode: some but not all fields provided 
     if not hasCustomParameters then
     -- Complete Mode: all fields provided
        templateMode = "documentation"
    local isPureDocumentation = not hasCustomParameters
     elseif hasAllParameters then
     local isPartialMode = hasCustomParameters and not hasAllParameters
        templateMode = "complete"
    local isCompleteMode = hasCustomParameters and hasAllParameters
     else
     local showInstructions = isPureDocumentation or isPartialMode
        templateMode = "partial"
     end
      
      
     -- Store mode flags for use in rendering
     -- Store mode state for rendering
     args._documentation_mode = isPureDocumentation
     args._template_mode = templateMode
     args._partial_mode = isPartialMode
     args._show_instructions = (templateMode ~= "complete")
    args._complete_mode = isCompleteMode
     args._campaign_data = campaignData
    args._show_instructions = showInstructions
     args._campaign_data = campaignData -- Store for usage instruction generation
      
      
     -- Defaults are only used for parameter documentation, not content rendering
     -- Defaults are only used for parameter documentation, not content rendering
Line 504: Line 370:
      
      
     -- Add usage instructions in documentation and partial modes
     -- Add usage instructions in documentation and partial modes
     if isPureDocumentation or isPartialMode then
     if templateMode ~= "complete" then
         table.insert(fields, {
         table.insert(fields, {
             key = "usageInstructions",
             key = "usageInstructions",
Line 510: Line 376:
             type = "text"
             type = "text"
         })
         })
        -- Set a dummy value so the field gets processed
         args.usageInstructions = "documentation"
         args.usageInstructions = "documentation"
     end
     end
Line 539: Line 404:
     else
     else
         -- Show placeholder for empty fields in documentation and partial modes
         -- Show placeholder for empty fields in documentation and partial modes
         if args._documentation_mode or args._partial_mode then
         if args._template_mode ~= "complete" then
             return "''Please see usage instructions above to customize this field.''"
             return "''Please see usage instructions above to customize this field.''"
         end
         end
Line 546: Line 411:
end
end


-- Special processor for campaign introduction - always uses JSON default, not user input
-- SPECIAL: campaign_intro processor - always uses JSON default, never user input
template._processors.campaign_intro = function(value, args, template, fieldType)
template._processors.campaign_intro = function(value, args, template, fieldType)
     -- Always use the campaign data default, ignore user input
     -- 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
     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
         local defaultIntro = args._campaign_data.defaults.campaign_intro
Line 554: Line 419:
     else
     else
         -- Fallback if no campaign data available
         -- Fallback if no campaign data available
         if args._documentation_mode or args._partial_mode then
         if args._template_mode ~= "complete" then
             return "''Campaign introduction will appear here from JSON defaults.''"
             return "''Campaign introduction will appear here from JSON defaults.''"
         end
         end
Line 573: Line 438:
end
end


-- Export the render function
function p.render(frame)
function p.render(frame)
     -- Create a custom render function that bypasses Blueprint's argument extraction
     template.current_frame = frame
    -- and handles arguments properly for direct module invocation
      
      
    template.current_frame = frame -- Store frame on template instance
     local depth = getRecursionDepth(frame)
 
    -- Check recursion depth to prevent infinite loops
     local depth = 0
    if frame.args and frame.args._recursion_depth then
        depth = tonumber(frame.args._recursion_depth) or 0
    elseif frame:getParent() and frame:getParent().args and frame:getParent().args._recursion_depth then
        depth = tonumber(frame:getParent().args._recursion_depth) or 0
    end
      
      
     if depth > 3 then
     if depth > 3 then
Line 600: Line 455:
     end
     end
      
      
     -- Handle arguments from both direct module invocation and template calls
     -- Merge arguments: frame args override parent args
     local args = {}
     local args = {}
   
    -- Get arguments from parent frame (template parameters)
     local parentArgs = frame:getParent().args or {}
     local parentArgs = frame:getParent().args or {}
     for k, v in pairs(parentArgs) do
     local frameArgs = frame.args or {}
        args[k] = v
    end
      
      
     -- Get arguments from current frame (module invocation parameters) - these take precedence
     for k, v in pairs(parentArgs) do args[k] = v end
    local frameArgs = frame.args or {}
     for k, v in pairs(frameArgs) do args[k] = v end
     for k, v in pairs(frameArgs) do
        args[k] = v
    end
      
      
    -- Normalize argument case like Blueprint does
    local TemplateHelpers = require('Module:TemplateHelpers')
     args = TemplateHelpers.normalizeArgumentCase(args)
     args = TemplateHelpers.normalizeArgumentCase(args)
      
      
    -- Increment recursion depth for any child template calls
     args._recursion_depth = tostring(depth + 1)
     args._recursion_depth = tostring(depth + 1)
      
      
    -- Run preprocessors manually (since we're bypassing Blueprint's renderTemplate)
    local Blueprint = require('Module:LuaTemplateBlueprint')
     args = Blueprint.runPreprocessors(template, args)
     args = Blueprint.runPreprocessors(template, args)
      
      
    -- Get table class configuration
    local tableClass = "template-table"
    if template.config.constants and template.config.constants.tableClass then
        tableClass = template.config.constants.tableClass
    end
   
    -- Set up structure configuration for rendering
     local structureConfig = {
     local structureConfig = {
         tableClass = tableClass,
         tableClass = (template.config.constants and template.config.constants.tableClass) or "template-table",
         blocks = {},
         blocks = {},
         containerTag = template.features.fullPage and "div" or "table"
         containerTag = template.features.fullPage and "div" or "table"
     }
     }
      
      
    -- Build rendering sequence manually
     local renderingSequence = Blueprint.buildRenderingSequence(template)
     local renderingSequence = Blueprint.buildRenderingSequence(template)
      
      
Line 646: Line 481:
     end
     end
      
      
    -- Add rendering functions to structure config
     for i = 1, renderingSequence._length do
     for i = 1, renderingSequence._length do
         table.insert(structureConfig.blocks, function(a)
         table.insert(structureConfig.blocks, function(a)
Line 653: Line 487:
     end
     end
      
      
    -- Render using TemplateStructure
    local TemplateStructure = require('Module:TemplateStructure')
     local result = TemplateStructure.render(args, structureConfig, template._errorContext)
     local result = TemplateStructure.render(args, structureConfig, template._errorContext)
      
      
     template.current_frame = nil -- Clear frame from template instance
     template.current_frame = nil
      
      
     return result
     return result