Jump to content

Module:WikitextProcessor: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(19 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:WikitextProcessor
--[[
-- Processes JSON content with wikitext formatting for frontend display
* Name: WikitextProcessor
-- Handles placeholder replacement, wiki link conversion to HTML, and content normalization
* Author: Mark W. Datysgeld
* Description: Generalized content processor for wikitext formatting and frontend display, regardless of source (JSON, XML, database, user input, etc.)
* Notes: Error handling; three wiki link patterns for page links, page links with custom text, and anchor links; placeholder replacement with $VARIABLE$ syntax; content normalization and whitespace cleanup; JavaScript escape hatch factory for NoticeHandler.js gadget integration to work around Scribunto/Lua environment-specific bugs
]]


local p = {}
local p = {}
Line 8: Line 11:
local ErrorHandling = require('Module:ErrorHandling')
local ErrorHandling = require('Module:ErrorHandling')


-- Helper function to determine appropriate context name for error reporting
-- Constants for performance
local function getContextName(errorContext)
local CONTEXT_NAME = 'wikitextProcessor'
    if not errorContext then
        return 'wikitextProcessor'
    end
   
    -- Check if this is a campaign context (backward compatibility)
    if errorContext.contextType == 'T-Campaign' or
      (errorContext.errors and errorContext.errors.campaignBanner) then
        return 'campaignBanner'  -- Maintain existing behavior
    end
   
    -- Default to generic context for new uses
    return errorContext.contextName or 'wikitextProcessor'
end
 
-- Error message mapping for backward compatibility
local ERROR_MESSAGES = {
local ERROR_MESSAGES = {
     -- Campaign-specific messages (preserve existing)
     urlFailed = 'URL generation failed for wiki link',
    campaignBanner = {
    encodeFailed = 'Text encoding failed for wiki link',
        urlFailed = 'mw.uri.fullUrl failed for page link',
    formatFailed = 'HTML formatting failed for wiki link'
        urlFailedPattern2 = 'Pattern 2 mw.uri.fullUrl failed',
        urlFailedPattern3 = 'Pattern 3 mw.uri.anchorEncode failed',
        encodeFailed = 'mw.text.encode failed for page link',
        encodeFailedPattern2 = 'Pattern 2 mw.text.encode failed',
        encodeFailedPattern3 = 'Pattern 3 mw.text.encode failed',
        formatFailed = 'string.format failed for page link',
        formatFailedPattern2 = 'Pattern 2 string.format failed',
        formatFailedPattern3 = 'Pattern 3 string.format failed'
    },
    -- Generic messages for new contexts
    wikitextProcessor = {
        urlFailed = 'URL generation failed for wiki link',
        urlFailedPattern2 = 'URL generation failed for wiki link',
        urlFailedPattern3 = 'Anchor encoding failed for wiki link',
        encodeFailed = 'Text encoding failed for wiki link',
        encodeFailedPattern2 = 'Text encoding failed for wiki link',
        encodeFailedPattern3 = 'Text encoding failed for wiki link',
        formatFailed = 'HTML formatting failed for wiki link',
        formatFailedPattern2 = 'HTML formatting failed for wiki link',
        formatFailedPattern3 = 'HTML formatting failed for wiki link'
    }
}
}


-- Get appropriate error message based on context
-- Clean error handling
local function getErrorMessage(contextName, messageType)
local function handleError(errorContext, operation, fallbackValue)
    local messages = ERROR_MESSAGES[contextName] or ERROR_MESSAGES.wikitextProcessor
    return messages[messageType] or messages.urlFailed
end
 
-- Unified error handling that preserves existing T-Campaign behavior
local function handleProcessingError(errorContext, operation, errorDetails, fallbackValue)
     if errorContext then
     if errorContext then
         local contextName = getContextName(errorContext)
         ErrorHandling.addStatus(errorContext, CONTEXT_NAME, ERROR_MESSAGES[operation] or ERROR_MESSAGES.urlFailed, nil)
        local message = getErrorMessage(contextName, operation)
       
        -- Preserve existing detailed error format for campaigns
        if contextName == 'campaignBanner' then
            ErrorHandling.addStatus(errorContext, contextName, message, errorDetails)
        else
            -- Simplified error reporting for new contexts
            ErrorHandling.addStatus(errorContext, contextName, message, nil)
        end
     end
     end
     return fallbackValue
     return fallbackValue
Line 81: Line 33:
         pattern = '%[%[([^#|%]]+)%]%]',
         pattern = '%[%[([^#|%]]+)%]%]',
         processor = function(pageName, errorContext)
         processor = function(pageName, errorContext)
            -- Step 1: Generate URL
             local spacesReplaced = (pageName:gsub(' ', '_'))
             local spacesReplaced = (pageName:gsub(' ', '_'))
             local success1, pageUrl = pcall(function()
             local success, pageUrl = pcall(function()
                 return tostring(mw.uri.fullUrl(spacesReplaced))
                 return tostring(mw.uri.fullUrl(spacesReplaced))
             end)
             end)
           
             if not success then
             if not success1 then
                 return handleError(errorContext, 'urlFailed', '[[' .. pageName .. ']]')
                 return handleProcessingError(errorContext, 'urlFailed',  
                    'Page: ' .. pageName .. ', Error: ' .. tostring(pageUrl),
                    '[[' .. pageName .. ']]')
             end
             end
              
              
            -- Step 2: Encode text
             local pageNameStr = type(pageName) == "string" and pageName or tostring(pageName)
             local pageNameStr = type(pageName) == "string" and pageName or tostring(pageName)
             local success2, encodedName = pcall(mw.text.encode, pageNameStr)
             success, pageNameStr = pcall(mw.text.encode, pageNameStr)
           
             if not success then
             if not success2 then
                 return handleError(errorContext, 'encodeFailed', '[[' .. pageName .. ']]')
                 return handleProcessingError(errorContext, 'encodeFailed',
                    'Page: ' .. pageName .. ', Error: ' .. tostring(encodedName),
                    '[[' .. pageName .. ']]')
             end
             end
              
              
             -- Step 3: Format HTML
             success, pageUrl = pcall(string.format, '<a href="%s">%s</a>', pageUrl, pageNameStr)
            local success3, result = pcall(string.format, '<a href="%s">%s</a>', pageUrl, encodedName)
             if not success then
           
                 return handleError(errorContext, 'formatFailed', '[[' .. pageName .. ']]')
             if not success3 then
                 return handleProcessingError(errorContext, 'formatFailed',
                    'Page: ' .. pageName .. ', Error: ' .. tostring(result),
                    '[[' .. pageName .. ']]')
             end
             end
              
              
             return result
             return pageUrl
         end
         end
     },
     },
Line 120: Line 60:
         pattern = '%[%[([^#|%]]+)|([^%]]+)%]%]',
         pattern = '%[%[([^#|%]]+)|([^%]]+)%]%]',
         processor = function(pageName, text, errorContext)
         processor = function(pageName, text, errorContext)
             local success1, pageUrl = pcall(function()
             local success, pageUrl = pcall(function()
                 return tostring(mw.uri.fullUrl((pageName:gsub(' ', '_'))))
                 return tostring(mw.uri.fullUrl((pageName:gsub(' ', '_'))))
             end)
             end)
           
             if not success then
             if not success1 then
                 return handleError(errorContext, 'urlFailed', '[[' .. pageName .. '|' .. text .. ']]')
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 2 mw.uri.fullUrl failed', 'Error: ' .. tostring(pageUrl))
                end
                return '[[' .. pageName .. '|' .. text .. ']]'
             end
             end
              
              
             local success2, encodedText = pcall(function()
             local textStr = type(text) == "string" and text or tostring(text)
                -- Ensure text is a string before encoding
            success, textStr = pcall(mw.text.encode, textStr)
                local textStr = type(text) == "string" and text or tostring(text)
             if not success then
                return mw.text.encode(textStr)
                 return handleError(errorContext, 'encodeFailed', '[[' .. pageName .. '|' .. text .. ']]')
            end)
           
             if not success2 then
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 2 mw.text.encode failed', 'Error: ' .. tostring(encodedText))
                end
                return '[[' .. pageName .. '|' .. text .. ']]'
             end
             end
              
              
             local success3, result = pcall(function()
             success, pageUrl = pcall(string.format, '<a href="%s">%s</a>', pageUrl, textStr)
                return string.format('<a href="%s">%s</a>', pageUrl, encodedText)
             if not success then
            end)
                 return handleError(errorContext, 'formatFailed', '[[' .. pageName .. '|' .. text .. ']]')
           
             if not success3 then
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 2 string.format failed', 'Error: ' .. tostring(result))
                end
                return '[[' .. pageName .. '|' .. text .. ']]'
             end
             end
              
              
             return result
             return pageUrl
         end
         end
     },
     },
Line 163: Line 86:
         pattern = '%[%[#([^|%]]+)|([^%]]+)%]%]',
         pattern = '%[%[#([^|%]]+)|([^%]]+)%]%]',
         processor = function(anchor, text, errorContext)
         processor = function(anchor, text, errorContext)
             local success1, encodedAnchor = pcall(function()
             local success, encodedAnchor = pcall(mw.uri.anchorEncode, anchor)
                return mw.uri.anchorEncode(anchor)
             if not success then
            end)
                 return handleError(errorContext, 'urlFailed', '[[#' .. anchor .. '|' .. text .. ']]')
           
             if not success1 then
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 3 mw.uri.anchorEncode failed', 'Error: ' .. tostring(encodedAnchor))
                end
                return '[[#' .. anchor .. '|' .. text .. ']]'
             end
             end
              
              
             local success2, encodedText = pcall(function()
             local textStr = type(text) == "string" and text or tostring(text)
                -- Ensure text is a string before encoding
            success, textStr = pcall(mw.text.encode, textStr)
                local textStr = type(text) == "string" and text or tostring(text)
             if not success then
                return mw.text.encode(textStr)
                 return handleError(errorContext, 'encodeFailed', '[[#' .. anchor .. '|' .. text .. ']]')
            end)
           
             if not success2 then
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 3 mw.text.encode failed', 'Error: ' .. tostring(encodedText))
                end
                return '[[#' .. anchor .. '|' .. text .. ']]'
             end
             end
              
              
             local success3, result = pcall(function()
             success, encodedAnchor = pcall(string.format, '<a href="#%s">%s</a>', encodedAnchor, textStr)
                return string.format('<a href="#%s">%s</a>', encodedAnchor, encodedText)
             if not success then
            end)
                 return handleError(errorContext, 'formatFailed', '[[#' .. anchor .. '|' .. text .. ']]')
           
             if not success3 then
                 if errorContext then
                    ErrorHandling.addStatus(errorContext, 'campaignBanner', 'Pattern 3 string.format failed', 'Error: ' .. tostring(result))
                end
                return '[[#' .. anchor .. '|' .. text .. ']]'
             end
             end
              
              
             return result
             return encodedAnchor
         end
         end
     }
     }
Line 277: Line 181:
      
      
     return processedContent
     return processedContent
end
--[[
    The "JavaScript Escape Hatch" Factory
    This function creates a data-only div intended to be processed by the
    NoticeHandler.js gadget. It serves as a workaround for a complex,
    environment-specific bug in Scribunto/Lua where string values would
    mysteriously disappear when passed through certain table operations.
    How it works:
    1.  Problem: Direct string processing and placeholder replacement in Lua
        was failing unpredictably. Byte-level analysis confirmed the string
        data was valid, but it would be lost during processing.
    2.  Solution: Instead of processing the content in Lua, we "escape" from
        the Lua environment. This function packages the raw, unprocessed
        content and any necessary parameters (like a title) into data-*
        attributes on an HTML element.
    3.  Handoff: This HTML element is then passed to the client-side, where
        the NoticeHandler.js gadget picks it up.
    4.  Execution: The JavaScript, running in the user's browser, reads the
        data attributes, performs the string replacements and wikitext
        processing, and injects the final HTML into the page.
    Architectural Note:
    This function is deliberately self-contained and does NOT call any other
    functions within this module (like processContentForFrontend). This is
    critical to prevent circular dependencies, as this function may be called
    by modules that are themselves dependencies of this one. It is a pure
        utility for generating the required HTML structure.
--]]
function p.createNoticeForJS(options)
    options = options or {}
    local noticeData = {
        type = options.type or "notice",
        position = options.position or "top",
        content = options.content or "",
        title = options.title or "",
        cssClass = options.cssClass or "notice-box"
    }
    local success, result = pcall(function()
        return string.format(
            '<div style="display:none" class="notice-data" data-notice-type="%s" data-notice-position="%s" data-banner-template="%s" data-banner-title="%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.title),
            mw.text.encode(noticeData.cssClass)
        )
    end)
    if success then
        return result
    else
        -- In case of error, return a simple error message.
        return '<span class="error">Error creating notice.</span>'
    end
end
end


return p
return p

Latest revision as of 03:14, 25 August 2025

Documentation for this module may be created at Module:WikitextProcessor/doc

--[[
* Name: WikitextProcessor
* Author: Mark W. Datysgeld
* Description: Generalized content processor for wikitext formatting and frontend display, regardless of source (JSON, XML, database, user input, etc.)
* Notes: Error handling; three wiki link patterns for page links, page links with custom text, and anchor links; placeholder replacement with $VARIABLE$ syntax; content normalization and whitespace cleanup; JavaScript escape hatch factory for NoticeHandler.js gadget integration to work around Scribunto/Lua environment-specific bugs
]]

local p = {}

-- Dependencies
local ErrorHandling = require('Module:ErrorHandling')

-- Constants for performance
local CONTEXT_NAME = 'wikitextProcessor'
local ERROR_MESSAGES = {
    urlFailed = 'URL generation failed for wiki link',
    encodeFailed = 'Text encoding failed for wiki link',
    formatFailed = 'HTML formatting failed for wiki link'
}

-- Clean error handling
local function handleError(errorContext, operation, fallbackValue)
    if errorContext then
        ErrorHandling.addStatus(errorContext, CONTEXT_NAME, ERROR_MESSAGES[operation] or ERROR_MESSAGES.urlFailed, nil)
    end
    return fallbackValue
end

-- Constants as upvalues for performance
local WIKI_LINK_PATTERNS = {
    -- Pattern 1: [[Page Name]] -> HTML page links
    {
        pattern = '%[%[([^#|%]]+)%]%]',
        processor = function(pageName, errorContext)
            local spacesReplaced = (pageName:gsub(' ', '_'))
            local success, pageUrl = pcall(function()
                return tostring(mw.uri.fullUrl(spacesReplaced))
            end)
            if not success then
                return handleError(errorContext, 'urlFailed', '[[' .. pageName .. ']]')
            end
            
            local pageNameStr = type(pageName) == "string" and pageName or tostring(pageName)
            success, pageNameStr = pcall(mw.text.encode, pageNameStr)
            if not success then
                return handleError(errorContext, 'encodeFailed', '[[' .. pageName .. ']]')
            end
            
            success, pageUrl = pcall(string.format, '<a href="%s">%s</a>', pageUrl, pageNameStr)
            if not success then
                return handleError(errorContext, 'formatFailed', '[[' .. pageName .. ']]')
            end
            
            return pageUrl
        end
    },
    
    -- Pattern 2: [[Page|text]] -> HTML page links with custom text
    {
        pattern = '%[%[([^#|%]]+)|([^%]]+)%]%]',
        processor = function(pageName, text, errorContext)
            local success, pageUrl = pcall(function()
                return tostring(mw.uri.fullUrl((pageName:gsub(' ', '_'))))
            end)
            if not success then
                return handleError(errorContext, 'urlFailed', '[[' .. pageName .. '|' .. text .. ']]')
            end
            
            local textStr = type(text) == "string" and text or tostring(text)
            success, textStr = pcall(mw.text.encode, textStr)
            if not success then
                return handleError(errorContext, 'encodeFailed', '[[' .. pageName .. '|' .. text .. ']]')
            end
            
            success, pageUrl = pcall(string.format, '<a href="%s">%s</a>', pageUrl, textStr)
            if not success then
                return handleError(errorContext, 'formatFailed', '[[' .. pageName .. '|' .. text .. ']]')
            end
            
            return pageUrl
        end
    },
    
    -- Pattern 3: [[#anchor|text]] -> HTML anchor links
    {
        pattern = '%[%[#([^|%]]+)|([^%]]+)%]%]',
        processor = function(anchor, text, errorContext)
            local success, encodedAnchor = pcall(mw.uri.anchorEncode, anchor)
            if not success then
                return handleError(errorContext, 'urlFailed', '[[#' .. anchor .. '|' .. text .. ']]')
            end
            
            local textStr = type(text) == "string" and text or tostring(text)
            success, textStr = pcall(mw.text.encode, textStr)
            if not success then
                return handleError(errorContext, 'encodeFailed', '[[#' .. anchor .. '|' .. text .. ']]')
            end
            
            success, encodedAnchor = pcall(string.format, '<a href="#%s">%s</a>', encodedAnchor, textStr)
            if not success then
                return handleError(errorContext, 'formatFailed', '[[#' .. anchor .. '|' .. text .. ']]')
            end
            
            return encodedAnchor
        end
    }
}

-- Normalizes content string by cleaning up whitespace
function p.normalizeContentString(content)
    if not content or content == "" then
        return content
    end
    
    -- Apply string normalization exactly like the original T-Campaign code
    return content:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
end

-- Replaces placeholder patterns ($VARIABLE$) with actual values
function p.replacePlaceholders(content, placeholderMap)
    if not content or not placeholderMap then
        return content
    end
    
    local result = content
    
    -- Apply placeholder replacement exactly like the original T-Campaign code
    for key, value in pairs(placeholderMap) do
        if value and value ~= "" then
            result = result:gsub("%$" .. key .. "%$", value)
        end
    end
    
    -- Clean up any remaining unfilled placeholders (TemplateStarter's removeEmptyPlaceholders logic)
    result = result:gsub("%$[A-Z_]+%$", "")
    result = result:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
    
    return result
end

-- Processes wiki links in content and converts them to HTML
function p.processWikiLinksToHTML(content, errorContext)
    if not content or content == "" then
        return content
    end
    
    -- UNIFIED PIPELINE: Process ALL wiki links with custom patterns (no frame:preprocess)
    local originalContent = content
    local result = content
    
    -- Process each wiki link pattern in sequence exactly like the original T-Campaign code
    for _, patternInfo in ipairs(WIKI_LINK_PATTERNS) do
        result = result:gsub(patternInfo.pattern, function(...)
            return patternInfo.processor(..., errorContext)
        end)
    end
    
    return result
end

-- Main entry point: Processes JSON content with wikitext formatting for frontend display
-- @param content The content string to process
-- @param placeholders Optional table of placeholder values for $VARIABLE$ replacement
-- @param errorContext Optional error context for error reporting
-- @return Processed content ready for frontend display
function p.processContentForFrontend(content, placeholders, errorContext)
    if not content or content == "" then
        return content
    end
    
    -- Step 1: Normalize content string
    local processedContent = p.normalizeContentString(content)
    
    -- Step 2: Replace placeholders if provided
    if placeholders then
        processedContent = p.replacePlaceholders(processedContent, placeholders)
    end
    
    -- Step 3: Process wiki links to HTML
    processedContent = p.processWikiLinksToHTML(processedContent, errorContext)
    
    return processedContent
end

--[[
    The "JavaScript Escape Hatch" Factory

    This function creates a data-only div intended to be processed by the
    NoticeHandler.js gadget. It serves as a workaround for a complex,
    environment-specific bug in Scribunto/Lua where string values would
    mysteriously disappear when passed through certain table operations.

    How it works:
    1.  Problem: Direct string processing and placeholder replacement in Lua
        was failing unpredictably. Byte-level analysis confirmed the string
        data was valid, but it would be lost during processing.
    2.  Solution: Instead of processing the content in Lua, we "escape" from
        the Lua environment. This function packages the raw, unprocessed
        content and any necessary parameters (like a title) into data-*
        attributes on an HTML element.
    3.  Handoff: This HTML element is then passed to the client-side, where
        the NoticeHandler.js gadget picks it up.
    4.  Execution: The JavaScript, running in the user's browser, reads the
        data attributes, performs the string replacements and wikitext
        processing, and injects the final HTML into the page.

    Architectural Note:
    This function is deliberately self-contained and does NOT call any other
    functions within this module (like processContentForFrontend). This is
    critical to prevent circular dependencies, as this function may be called
    by modules that are themselves dependencies of this one. It is a pure
        utility for generating the required HTML structure.
--]]
function p.createNoticeForJS(options)
    options = options or {}
    local noticeData = {
        type = options.type or "notice",
        position = options.position or "top",
        content = options.content or "",
        title = options.title or "",
        cssClass = options.cssClass or "notice-box"
    }

    local success, result = pcall(function()
        return string.format(
            '<div style="display:none" class="notice-data" data-notice-type="%s" data-notice-position="%s" data-banner-template="%s" data-banner-title="%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.title),
            mw.text.encode(noticeData.cssClass)
        )
    end)

    if success then
        return result
    else
        -- In case of error, return a simple error message.
        return '<span class="error">Error creating notice.</span>'
    end
end

return p