Module:T-Campaign: Difference between revisions
Appearance
// via Wikitext Extension for VSCode Tag: Reverted |
// via Wikitext Extension for VSCode Tags: Manual revert Reverted |
||
| Line 1: | Line 1: | ||
-- | -- T-Campaign.lua | ||
-- Generic campaign template that dynamically loads campaign data from JSON files | |||
-- 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 = | errorReporting = true | ||
} | } | ||
}) | }) | ||
| Line 24: | Line 24: | ||
-- Dynamic configuration (from JSON) | -- Dynamic configuration (from JSON) | ||
template.config = { | template.config = { | ||
fields = {}, | fields = {}, | ||
categories = { | categories = { base = {} }, | ||
constants = { | constants = { | ||
title = "Campaign Information", | title = "Campaign Information", | ||
tableClass = "" | tableClass = "" | ||
} | } | ||
} | } | ||
Blueprint.initializeConfig(template) | Blueprint.initializeConfig(template) | ||
template.config.blockSequence = { | template.config.blockSequence = { | ||
'campaignBanner', | 'campaignBanner', | ||
'campaignTitle', | 'campaignTitle', | ||
'campaignInstructions', | 'campaignInstructions', | ||
'campaignContent', | 'campaignContent', | ||
| Line 47: | Line 43: | ||
} | } | ||
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 | -- Helper function: Get campaign name with simplified fallback logic | ||
local function getCampaignName(args, campaignData) | 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 | if not campaignName then | ||
campaignName = "CAMPAIGN_NAME" | campaignName = (campaignData and campaignData.template_id) or "CAMPAIGN_NAME" | ||
end | end | ||
| Line 144: | Line 135: | ||
end | 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 = { | template.config.blocks.campaignBanner = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
render = function(template, args) | render = function(template, args) | ||
local context = template._errorContext | 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 ErrorHandling.formatCombinedOutput(context) | ||
end | end | ||
| Line 164: | Line 163: | ||
end | end | ||
local placeholderValues = { | local placeholderValues = { | ||
CAMPAIGN_NAME = args._campaign_data.defaults.title or "Campaign" | CAMPAIGN_NAME = args._campaign_data.defaults.title or "Campaign" | ||
} | } | ||
bannerContent = WikitextProcessor.processContentForFrontend(bannerContent, placeholderValues, context) | |||
local noticeData = { | local noticeData = { | ||
type = "campaign", | type = "campaign", | ||
| Line 304: | Line 176: | ||
} | } | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
return string.format( | return string.format( | ||
| Line 324: | Line 195: | ||
} | } | ||
-- | -- Campaign Title | ||
template.config.blocks.campaignTitle = { | template.config.blocks.campaignTitle = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
| Line 333: | Line 204: | ||
} | } | ||
-- | -- 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 "" | return "" | ||
end | end | ||
| Line 348: | Line 219: | ||
} | } | ||
-- | -- 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 | ||
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 | ||
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 | ||
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 ( | -- CRITICAL: Get campaign_name from args (merged by Blueprint) or current frame for direct module calls | ||
local campaignName = args.campaign_name | local campaignName = args.campaign_name | ||
-- | -- 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 | template._errorContext, | ||
'campaignLoader', | 'campaignLoader', | ||
'Campaign loaded successfully for ' .. campaignName, | 'Campaign loaded successfully for ' .. campaignName, | ||
| Line 451: | Line 318: | ||
) | ) | ||
-- Detect | -- 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 | ||
local templateMode | |||
if not hasCustomParameters then | |||
templateMode = "documentation" | |||
elseif hasAllParameters then | |||
templateMode = "complete" | |||
else | |||
templateMode = "partial" | |||
end | |||
-- Store mode | -- Store mode state for rendering | ||
args. | args._template_mode = templateMode | ||
args. | args._show_instructions = (templateMode ~= "complete") | ||
args._campaign_data = campaignData | |||
args._campaign_data = campaignData | |||
-- 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 | if templateMode ~= "complete" then | ||
table.insert(fields, { | table.insert(fields, { | ||
key = "usageInstructions", | key = "usageInstructions", | ||
| Line 510: | Line 376: | ||
type = "text" | type = "text" | ||
}) | }) | ||
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. | 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: 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 | -- 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. | 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 | ||
function p.render(frame) | function p.render(frame) | ||
template.current_frame = frame | |||
local depth = getRecursionDepth(frame) | |||
local depth = | |||
if depth > 3 then | if depth > 3 then | ||
| Line 600: | Line 455: | ||
end | end | ||
-- | -- Merge arguments: frame args override parent args | ||
local args = {} | local args = {} | ||
local parentArgs = frame:getParent().args or {} | 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 | |||
for k, v in pairs(frameArgs) do | |||
args = TemplateHelpers.normalizeArgumentCase(args) | args = TemplateHelpers.normalizeArgumentCase(args) | ||
args._recursion_depth = tostring(depth + 1) | args._recursion_depth = tostring(depth + 1) | ||
args = Blueprint.runPreprocessors(template, args) | args = Blueprint.runPreprocessors(template, args) | ||
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" | ||
} | } | ||
local renderingSequence = Blueprint.buildRenderingSequence(template) | local renderingSequence = Blueprint.buildRenderingSequence(template) | ||
| Line 646: | Line 481: | ||
end | end | ||
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 | ||
local result = TemplateStructure.render(args, structureConfig, template._errorContext) | local result = TemplateStructure.render(args, structureConfig, template._errorContext) | ||
template.current_frame = nil | template.current_frame = nil | ||
return result | return result | ||
Revision as of 00:32, 2 August 2025
Documentation for this module may be created at Module:T-Campaign/doc
-- T-Campaign.lua
-- Generic campaign template that dynamically loads campaign data from JSON files
-- Usage: {{#invoke:T-Campaign|render|campaign_name=ASP2025}}
local p = {}
-- Required modules
local Blueprint = require('Module:LuaTemplateBlueprint')
local ErrorHandling = require('Module:ErrorHandling')
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
local template = Blueprint.registerTemplate('Campaign', {
features = {
fullPage = true, -- Full-page layout
categories = true,
errorReporting = true
}
})
-- Dynamic configuration (from JSON)
template.config = {
fields = {},
categories = { base = {} },
constants = {
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: 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 .. "}}"
-- Build instruction content
table.insert(output, "'''READ CAREFULLY'''. These are temporary instructions that will appear only until all parameters outlined here have been filled. Choose 'Edit source' at any time and edit the code below if it already exists. It can also be added manually to any page:")
table.insert(output, "")
table.insert(output, "<pre>" .. templateCall .. "</pre>")
table.insert(output, "")
table.insert(output, "'''Available Parameters:'''")
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 cssClass = banner.css_class or "campaign-banner"
if bannerContent == "" then
ErrorHandling.addStatus(context, 'campaignBanner', 'Empty banner content', 'No content to display')
return ErrorHandling.formatCombinedOutput(context)
end
local placeholderValues = {
CAMPAIGN_NAME = args._campaign_data.defaults.title or "Campaign"
}
bannerContent = WikitextProcessor.processContentForFrontend(bannerContent, placeholderValues, context)
local noticeData = {
type = "campaign",
position = "top",
content = bannerContent,
cssClass = cssClass
}
local success, result = pcall(function()
return string.format(
'<div style="display:none" class="notice-data" data-notice-type="%s" data-notice-position="%s" data-notice-content="%s" data-notice-css="%s"></div>',
mw.text.encode(noticeData.type),
mw.text.encode(noticeData.position),
mw.text.encode(noticeData.content),
mw.text.encode(noticeData.cssClass)
)
end)
if success then
return result .. ErrorHandling.formatCombinedOutput(context)
else
ErrorHandling.addError(context, 'campaignBanner', 'Data attribute creation failed', tostring(result), false)
return ErrorHandling.formatCombinedOutput(context)
end
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
local function processFieldValue(value, fieldType)
if type(value) == "table" then
if #value > 0 then
-- Array of values - render as bullet list
return "* " .. table.concat(value, "\n* ")
else
-- Object with key-value pairs - render as sections
local output = {}
for category, content in pairs(value) do
local categoryTitle = "'''" .. category:gsub("^%l", string.upper):gsub("_", " ") .. "'''"
table.insert(output, categoryTitle)
if type(content) == "table" and #content > 0 then
table.insert(output, "* " .. table.concat(content, "\n* "))
else
table.insert(output, tostring(content))
end
end
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 ~= "" then
table.insert(items, '<span class="campaign-token">' .. trimmed .. '</span>')
end
end
if #items > 0 then
return table.concat(items, ' ')
else
return tostring(value)
end
else
return tostring(value)
end
end
-- Custom preprocessor to load campaign data and generate fields dynamically
Blueprint.addPreprocessor(template, function(template, args)
-- CRITICAL: Get campaign_name from args (merged by Blueprint) or current frame for direct module calls
local campaignName = args.campaign_name
-- Fallback for direct module invocations where args may not be merged yet
if (not campaignName or campaignName == "") and template.current_frame then
local frameArgs = template.current_frame.args or {}
campaignName = frameArgs.campaign_name
end
if not campaignName or campaignName == "" then
return args
end
-- Load campaign data from JSON
local campaignData = DatasetLoader.get('Campaigns/' .. campaignName)
if not campaignData or not campaignData.defaults or not campaignData.field_definitions then
return args
end
-- 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 = {}
-- Always show campaign content fields (they'll show placeholder text when empty)
for _, fieldDef in ipairs(campaignData.field_definitions) do
table.insert(fields, {
key = fieldDef.key,
label = fieldDef.label,
type = fieldDef.type
})
end
-- Add usage instructions in documentation and partial modes
if templateMode ~= "complete" then
table.insert(fields, {
key = "usageInstructions",
label = "Usage Instructions",
type = "text"
})
args.usageInstructions = "documentation"
end
template.config.fields = fields
-- Override title if provided in JSON defaults
if campaignData.defaults.title then
template.config.constants.title = campaignData.defaults.title
end
-- Add campaign-specific category
template.config.categories.base = {campaignName}
return args
end)
-- Initialize field processors for the template
-- Set up a universal processor that can handle any field type
if not template._processors then
template._processors = {}
end
-- Set up a universal field processor that handles all field types
template._processors.default = function(value, args, template, fieldType)
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
-- Special processor for usage instructions
template._processors.usageInstructions = function(value, args, template)
if not args._show_instructions or not args._campaign_data then
return nil -- Only render when instructions should be shown
end
local campaignName = getCampaignName(args, args._campaign_data)
local instructions = generateUsageInstructions(campaignName, args._campaign_data)
return "\n----\n'''Usage Instructions'''\n\n" .. instructions
end
function p.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
return p