Module:T-Campaign: Difference between revisions
// via Wikitext Extension for VSCode Tags: Manual revert Reverted |
// via Wikitext Extension for VSCode |
||
| (19 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- | -- Module:T-Campaign.lua | ||
-- Generic campaign template that dynamically loads campaign data from JSON files | |||
-- Usage: {{#invoke:T-Campaign|render|campaign_name=NAME}} | |||
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) | ||
-- 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 | 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 | if bannerContent == "" then | ||
| Line 164: | Line 185: | ||
end | end | ||
-- | -- Use the centralized NoticeFactory to create the notice | ||
local noticeOptions = { | |||
type = "campaign-js", | |||
local | |||
type = "campaign", | |||
position = "top", | position = "top", | ||
content = bannerContent, | content = bannerContent, | ||
title = titleText, | |||
cssClass = cssClass | cssClass = cssClass | ||
} | } | ||
return WikitextProcessor.createNoticeForJS(noticeOptions) .. ErrorHandling.formatCombinedOutput(context) | |||
end | end | ||
} | } | ||
-- | -- Campaign Title | ||
template.config.blocks.campaignTitle = { | template.config.blocks.campaignTitle = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
| Line 333: | Line 207: | ||
} | } | ||
-- | -- 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 222: | ||
} | } | ||
-- | -- Campaign Content | ||
template.config.blocks.campaignContent = { | template.config.blocks.campaignContent = { | ||
feature = 'fullPage', | feature = 'fullPage', | ||
| Line 355: | Line 229: | ||
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 234: | ||
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 255: | ||
-- 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 282: | ||
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 289: | ||
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 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 ( | -- 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 319: | ||
-- 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 325: | ||
) | ) | ||
-- 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 347: | ||
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 496: | Line 369: | ||
-- Always show campaign content fields (they'll show placeholder text when empty) | -- Always show campaign content fields (they'll show placeholder text when empty) | ||
for _, fieldDef in ipairs(campaignData.field_definitions) do | for _, fieldDef in ipairs(campaignData.field_definitions) do | ||
table.insert(fields, { | -- CRITICAL: Skip 'title' as it is not a content field | ||
if fieldDef.key ~= "title" then | |||
table.insert(fields, { | |||
key = fieldDef.key, | |||
label = fieldDef.label, | |||
type = fieldDef.type | |||
}) | |||
end | |||
end | end | ||
-- 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 386: | ||
type = "text" | type = "text" | ||
}) | }) | ||
args.usageInstructions = "documentation" | args.usageInstructions = "documentation" | ||
end | end | ||
| Line 521: | Line 396: | ||
end | end | ||
-- Add campaign-specific category | -- Add campaign-specific category, defaulting to template_id | ||
template.config.categories.base = { | local category_value = campaignData.category or campaignData.template_id | ||
template.config.categories.base = {category_value} | |||
return args | return args | ||
| Line 539: | Line 415: | ||
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 422: | ||
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 430: | ||
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 449: | ||
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 466: | ||
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 492: | ||
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 498: | ||
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 | ||