Module:T-Campaign: Difference between revisions
// via Wikitext Extension for VSCode Tags: Manual revert Reverted |
// via Wikitext Extension for VSCode Tag: 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 {} | ||
-- 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 | -- Helper function: Tokenize semicolon-separated strings for instructions | ||
| Line 92: | Line 96: | ||
templateCall = templateCall .. "}}" | 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, "") | ||
table.insert(output, "<pre>" .. templateCall .. "</pre>") | table.insert(output, "<pre>" .. templateCall .. "</pre>") | ||
table.insert(output, "") | table.insert(output, "") | ||
table.insert(output, | table.insert(output, parameterIntro) | ||
if campaignData.field_definitions then | if campaignData.field_definitions then | ||
| Line 127: | Line 138: | ||
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) | ||
local campaignName = args.campaign_name | -- 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 151: | ||
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 | ||
local banner = args._campaign_data.banner | local banner = args._campaign_data.banner | ||
local bannerContent = banner.content or "" | local bannerContent = banner.content or "" | ||
local cssClass = banner.css_class | -- 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 | if bannerContent == "" then | ||
| Line 164: | Line 183: | ||
end | end | ||
-- | -- Ensure proper type checking and string conversion for placeholder values | ||
local campaignTitle = "Campaign" -- Default fallback | |||
if args._campaign_data and args._campaign_data.defaults and args._campaign_data.defaults.title then | |||
campaignTitle = tostring(args._campaign_data.defaults.title) | |||
end | |||
local placeholderValues = { | local placeholderValues = { | ||
CAMPAIGN_NAME = | CAMPAIGN_NAME = campaignTitle | ||
} | } | ||
-- | -- Add debugging to see what we're actually passing | ||
ErrorHandling.addStatus(context, 'campaignBanner', 'Placeholder values prepared', 'CAMPAIGN_NAME = "' .. campaignTitle .. '"') | |||
bannerContent = WikitextProcessor.processContentForFrontend(bannerContent, placeholderValues, context) | |||
bannerContent = | |||
local noticeData = { | local noticeData = { | ||
type = "campaign", | type = "campaign", | ||
| Line 304: | Line 205: | ||
} | } | ||
local success, result = pcall(function() | local success, result = pcall(function() | ||
return string.format( | return string.format( | ||
| Line 324: | Line 224: | ||
} | } | ||
-- | -- Campaign Title | ||
template.config.blocks.campaignTitle = { | template.config.blocks.campaignTitle = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
| Line 333: | Line 233: | ||
} | } | ||
-- | -- 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 248: | ||
} | } | ||
-- | -- Campaign Content | ||
template.config.blocks.campaignContent = { | template.config.blocks.campaignContent = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
| Line 355: | Line 255: | ||
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 260: | ||
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 384: | Line 281: | ||
-- Generic field processor that handles different data types | -- Generic field processor that handles different data types | ||
local function processFieldValue(value, fieldType) | 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 407: | Line 308: | ||
for item in value:gmatch("[^;]+") do | for item in value:gmatch("[^;]+") do | ||
local trimmed = item:match("^%s*(.-)%s*$") -- trim whitespace | local trimmed = item:match("^%s*(.-)%s*$") -- trim whitespace | ||
if trimmed and trimmed ~= "" then | if trimmed and trimmed ~= "" and not isNotApplicable(trimmed) then | ||
table.insert(items, '<span class="campaign-token">' .. trimmed .. '</span>') | table.insert(items, '<span class="campaign-token">' .. trimmed .. '</span>') | ||
end | end | ||
| Line 414: | Line 315: | ||
return table.concat(items, ' ') | return table.concat(items, ' ') | ||
else | else | ||
return | return nil -- Return nil if all items were "not applicable" | ||
end | end | ||
else | else | ||
| Line 423: | Line 324: | ||
-- 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 345: | ||
-- 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 351: | ||
) | ) | ||
-- 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 373: | ||
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 403: | ||
-- 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 409: | ||
type = "text" | type = "text" | ||
}) | }) | ||
args.usageInstructions = "documentation" | args.usageInstructions = "documentation" | ||
end | end | ||
| Line 539: | Line 437: | ||
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 444: | ||
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 452: | ||
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 471: | ||
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 488: | ||
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 514: | ||
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 520: | ||
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 | ||