Module:LuaTemplateBlueprint: Difference between revisions

// via Wikitext Extension for VSCode
Tag: Manual revert
// via Wikitext Extension for VSCode
 
(45 intermediate revisions by the same user not shown)
Line 65: Line 65:
-- Wiki link pattern for regex matching
-- Wiki link pattern for regex matching
local WIKI_LINK_PATTERN = '%[%[.-%]%]'
local WIKI_LINK_PATTERN = '%[%[.-%]%]'
-- Semantic property HTML templates
local PROPERTY_DIV_PREFIX = '<div style="display:none;">\n  {{#set: '
local PROPERTY_DIV_SUFFIX = ' }}\n</div>'
-- Empty table for returning when needed
-- Empty table for returning when needed
local EMPTY_OBJECT = {}
local EMPTY_OBJECT = {}
Line 75: Line 72:
local ErrorHandling = require('Module:ErrorHandling')
local ErrorHandling = require('Module:ErrorHandling')
local ConfigRepository = require('Module:ConfigRepository')
local ConfigRepository = require('Module:ConfigRepository')
local ConfigHelpers = require('Module:ConfigHelpers')
local TemplateHelpers = require('Module:TemplateHelpers')
local TemplateHelpers = require('Module:TemplateHelpers')
local TemplateStructure = require('Module:TemplateStructure')
local TemplateStructure = require('Module:TemplateStructure')
Line 343: Line 341:


-- ========== Configuration Integration ==========
-- ========== Configuration Integration ==========
-- Standard configuration sections used by templates
p.configSections = {
    'meta',
    'categories',
    'patterns',
    'fields',
    'mappings',
    'constants',
    'semantics'
}
-- Initialize the standard configuration for a template
-- Initialize the standard configuration for a template
-- Combines base config from ConfigRepository with template overrides
-- Combines base config from ConfigRepository with template overrides
Line 361: Line 348:
     local templateType = template.type
     local templateType = template.type
     local configOverrides = template.config or {}
     local configOverrides = template.config or {}
   
 
     -- Get base configuration from repository
     -- Get base configuration from repository
     local baseConfig = ConfigRepository.getStandardConfig(templateType)
     local baseConfig = ConfigRepository.getStandardConfig(templateType)
   
 
     -- Apply overrides to each section
     -- Use ConfigHelpers to deep merge configurations
     local config = {}
     local config = ConfigHelpers.deepMerge(baseConfig, configOverrides)
    for _, section in ipairs(p.configSections) do
 
        config[section] = config[section] or {}
       
        -- Copy base config for this section if available
        if baseConfig[section] then
            for k, v in pairs(baseConfig[section]) do
                config[section][k] = v
            end
        end
       
        -- Apply overrides for this section if available
        if configOverrides[section] then
            for k, v in pairs(configOverrides[section]) do
                config[section][k] = v
            end
        end
    end
   
     -- Store complete config in template
     -- Store complete config in template
     template.config = config
     template.config = config
Line 398: Line 368:
         end)
         end)
     end
     end
   
 
     return config
     return config
end
end
Line 439: Line 409:
                     args,
                     args,
                     TEMPLATE_TITLE_CLASS_PREFIX .. string.lower(templateId),
                     TEMPLATE_TITLE_CLASS_PREFIX .. string.lower(templateId),
                     titleText
                     titleText,
                    template.titleId
                 )
                 )
                 end,
                 end,
Line 703: Line 674:
     template._processors = TemplateFieldProcessor.initializeProcessors(template)
     template._processors = TemplateFieldProcessor.initializeProcessors(template)
     return template._processors
     return template._processors
end
-- Get field value from args (delegated to TemplateHelpers)
-- @param field table The field definition
-- @param args table The template arguments
-- @return string|nil The field value or nil if not found
function p.getFieldValue(field, args, template)
    local _, value = TemplateHelpers.getFieldValue(args, field)
    return value
end
end


Line 848: Line 810:
end
end


-- Process a value using a transform function
-- Validate property value to prevent SMW parser issues
-- @param value string The value to transform
-- @param value string The value to validate
-- @param property string The property name (for error context)
-- @return string The validated value
-- @param transform function The transform function
local function validatePropertyValue(value)
-- @param args table The template arguments
-- @param template table The template object
-- @return string The transformed value
local function applyTransform(value, property, transform, args, template)
     if not value or value == '' then
     if not value or value == '' then
         return ''
         return ''
     end
     end
      
      
     if not transform then
     -- Convert to string if needed
        return value
    value = tostring(value)
     end
   
    -- Remove potentially problematic wiki markup
    value = value:gsub('{{.-}}', '')  -- Remove template calls
     value = value:gsub('%[%[Category:.-]]', '')  -- Remove categories
      
      
     local transformedValue = p.protectedExecute(
     -- Escape pipe characters that might break SMW
        template,
    value = value:gsub('|', '{{!}}')
        'Transform_' .. property,
        function() return transform(value, args, template) end,
        value,
        value,
        args,
        template
    )
      
      
     return transformedValue or ''
     return value
end
end


Line 895: Line 849:
       (not template._propertyProviders or #template._propertyProviders == 0) then
       (not template._propertyProviders or #template._propertyProviders == 0) then
         return EMPTY_STRING
         return EMPTY_STRING
    end
   
    -- Set time budget for semantic property processing (450ms)
    local startTime = os.clock()
    local timeLimit = 0.45  -- seconds
    local checkInterval = 10  -- check every N properties
    local propertyCounter = 0
   
    local function checkTimeLimit()
        propertyCounter = propertyCounter + 1
        if propertyCounter % checkInterval == 0 then
            if os.clock() - startTime > timeLimit then
                return true  -- time exceeded
            end
        end
        return false
     end
     end
      
      
Line 902: Line 872:
     }
     }
      
      
     -- Collect all properties in a single batch for complete deduplication
     -- Build initial property mapping (like original code)
     local allProperties = {}
     local allProperties = {}
      
      
     -- Add basic properties with transforms handled by SemanticAnnotations
     -- Process basic properties - just map property names to field names
     for property, param in pairs(properties) do
     for property, param in pairs(properties) do
         -- Perform case-insensitive lookup for the parameter key
         if not skipProperties[property] then
        local keyName, _ = TemplateHelpers.getFieldValue(args, { key = param })
            -- Just map the property to the field name
        allProperties[property] = keyName or param
            -- SemanticAnnotations will handle value extraction and transforms
            allProperties[property] = param
        end
     end
     end
      
      
     -- Process additional properties with transforms
    -- Create collector for deduplication of additional properties and providers
    local collector = {
        seen = {},        -- Track property:value signatures
        properties = {},  -- Final deduplicated properties
        count = 0        -- Track total property count
    }
   
     -- Process additional properties with early deduplication and multi-value handling
     for property, fields in pairs(additionalProperties) do
     for property, fields in pairs(additionalProperties) do
         -- Skip properties that are explicitly marked to skip
         -- Skip properties that are explicitly marked to skip
Line 920: Line 899:
                 local rawValue = args[fieldName]
                 local rawValue = args[fieldName]
                 if rawValue and rawValue ~= '' then
                 if rawValue and rawValue ~= '' then
                     local transformed = applyTransform(rawValue, property, transform, args, template)
                    -- Handle multi-value fields by splitting first
                    if transformed and transformed ~= '' then
                     local values
                        local existing = allProperties[property]
                    if rawValue:find(';') then
                        if existing == nil then
                        values = TemplateHelpers.splitMultiValueString(rawValue)
                            allProperties[property] = transformed
                    else
                        else
                        values = {rawValue}
                            if type(existing) ~= "table" then
                    end
                                 allProperties[property] = { existing }
                   
                    -- Process each value individually
                    for _, singleValue in ipairs(values) do
                        local trimmedValue = singleValue:match("^%s*(.-)%s*$")
                        if trimmedValue and trimmedValue ~= '' then
                            -- Apply transform if available
                            local finalValue = trimmedValue
                            if transform then
                                finalValue = p.protectedExecute(
                                    template,
                                    'Transform_' .. property,
                                    function() return transform(trimmedValue, args, template) end,
                                    trimmedValue,
                                    trimmedValue,
                                    args,
                                    template
                                 )
                             end
                             end
                             -- Add only if not already present
                           
                             local found = false
                             -- Validate and add to collector
                             for _, v in ipairs(allProperties[property]) do
                             finalValue = validatePropertyValue(finalValue)
                                if v == transformed then
                             if finalValue and finalValue ~= '' then
                                     found = true
                                if not collector.properties[property] then
                                    break
                                     collector.properties[property] = {}
                                 end
                                 end
                            end
                                 table.insert(collector.properties[property], finalValue)
                            if not found then
                                collector.count = collector.count + 1
                                 table.insert(allProperties[property], transformed)
                             end
                             end
                         end
                         end
Line 947: Line 941:
     end
     end
      
      
    -- Process country and region properties with the new batch-friendly approach
    -- if a country field exists and country is not in skipProperties
    if args.country and args.country ~= '' and not skipProperties["Has country"] then
        -- Load CountryData lazily to avoid circular dependencies
        local CountryData = require('Module:CountryData')
        local countryProperties = p.protectedExecute(
            template,
            'CountryData_Properties',
            function() return CountryData.getSemanticCountryRegionProperties(args.country) end,
            {},
            args.country
        )
       
        -- Merge country properties into allProperties with deduplication
        if countryProperties and next(countryProperties) then
            for property, values in pairs(countryProperties) do
                -- Skip if explicitly marked to skip (double-check)
                if not skipProperties[property] then
                    if not allProperties[property] then
                        -- Property doesn't exist yet, add directly
                        allProperties[property] = values
                    else
                        -- Property already exists, merge with existing values
                        if type(allProperties[property]) ~= "table" then
                            -- Convert existing value to array
                            allProperties[property] = {allProperties[property]}
                        end
                       
                        -- Add unique values
                        local seenValues = {}
                        for _, v in ipairs(allProperties[property]) do
                            seenValues[v] = true
                        end
                       
                        for _, v in ipairs(values) do
                            if not seenValues[v] then
                                seenValues[v] = true
                                table.insert(allProperties[property], v)
                            end
                        end
                    end
                end
            end
        end
    end
      
      
     -- Process property providers with deduplication
     -- Process property providers with early deduplication
     if template._propertyProviders then
     if template._propertyProviders then
         for _, provider in ipairs(template._propertyProviders) do
         for _, provider in ipairs(template._propertyProviders) do
Line 1,006: Line 955:
              
              
             if providerResult and next(providerResult) then
             if providerResult and next(providerResult) then
                 -- Merge provider properties into allProperties with deduplication
                 -- Process provider properties through deduplication
                 for property, value in pairs(providerResult) do
                 for property, value in pairs(providerResult) do
                     -- Skip properties marked to skip
                     -- Skip properties marked to skip
Line 1,012: Line 961:
                         if type(value) == "table" then
                         if type(value) == "table" then
                             -- Provider returned an array of values
                             -- Provider returned an array of values
                             if allProperties[property] then
                             for _, v in ipairs(value) do
                                -- Property already exists, merge arrays with deduplication
                                 local validated = validatePropertyValue(v)
                                if type(allProperties[property]) ~= "table" then
                                 if validated and validated ~= '' then
                                    -- Convert existing value to array
                                     if not collector.properties[property] then
                                    allProperties[property] = {allProperties[property]}
                                         collector.properties[property] = {}
                                end
                               
                                -- Track seen values
                                local seenValues = {}
                                for _, v in ipairs(allProperties[property]) do
                                    seenValues[v] = true
                                 end
                               
                                -- Add unique values from provider
                                for _, v in ipairs(value) do
                                    if not seenValues[v] then
                                        seenValues[v] = true
                                        table.insert(allProperties[property], v)
                                    end
                                 end
                            else
                                -- Property doesn't exist yet, add with internal deduplication
                                local seenValues = {}
                                local uniqueValues = {}
                               
                                for _, v in ipairs(value) do
                                     if not seenValues[v] then
                                         seenValues[v] = true
                                        table.insert(uniqueValues, v)
                                     end
                                     end
                                    table.insert(collector.properties[property], validated)
                                    collector.count = collector.count + 1
                                 end
                                 end
                               
                                allProperties[property] = uniqueValues
                             end
                             end
                         else
                         else
                             -- Provider returned a single value
                             -- Provider returned a single value
                             if allProperties[property] then
                             local validated = validatePropertyValue(value)
                                -- Property already exists
                            if validated and validated ~= '' then
                                if type(allProperties[property]) == "table" then
                                if not collector.properties[property] then
                                    -- Existing property is an array, add value if unique
                                     collector.properties[property] = {}
                                    local seen = false
                                    for _, v in ipairs(allProperties[property]) do
                                        if v == value then
                                            seen = true
                                            break
                                        end
                                    end
                                   
                                    if not seen then
                                        table.insert(allProperties[property], value)
                                    end
                                elseif allProperties[property] ~= value then
                                     -- Convert to array with both values
                                    allProperties[property] = {allProperties[property], value}
                                 end
                                 end
                                 -- If existing value equals new value, no change needed
                                 table.insert(collector.properties[property], validated)
                            else
                                 collector.count = collector.count + 1
                                -- Property doesn't exist yet, add directly
                                 allProperties[property] = value
                             end
                             end
                         end
                         end
Line 1,087: Line 996:
         and centralizes normalization logic for clarity and consistency.
         and centralizes normalization logic for clarity and consistency.
     ]]
     ]]
     -- Override raw country/region with normalized names
     -- Override raw country/region with normalized names if country field exists
     local cr = require('Module:ConfigRepository')
     if args.country and args.country ~= '' then
    local cd = require('Module:CountryData')
        local cr = require('Module:ConfigRepository')
    local norm = p.protectedExecute(
        local cd = require('Module:CountryData')
        template,
        local norm = p.protectedExecute(
        'CountryData_Override',
            template,
        function() return cd.getSemanticCountryRegionProperties(args.country) end,
            'CountryData_Override',
         {},
            function() return cd.getSemanticCountryRegionProperties(args.country) end,
         args.country
            {},
            args.country
        )
        if norm then
            local countryKey = cr.semanticProperties.country
            local regionKey = cr.semanticProperties.region
            if norm[countryKey] then
                collector.properties[countryKey] = norm[countryKey]
            end
            if norm[regionKey] then
                collector.properties[regionKey] = norm[regionKey]
            end
        end
    end
   
    -- Merge basic properties mapping with deduplicated additional properties
    -- Basic properties (allProperties) contains field mappings
    -- Additional properties (collector.properties) contains actual values
    local finalProperties = {}
   
    -- Copy basic property mappings
    for property, fieldName in pairs(allProperties) do
        finalProperties[property] = fieldName
    end
   
    -- Add deduplicated additional properties (these have actual values)
    for property, value in pairs(collector.properties) do
         finalProperties[property] = value -- This might be an array of values or a single value
    end
 
    -- Process fixed properties
    if semanticConfig.fixedProperties and type(semanticConfig.fixedProperties) == 'table' then
        for propName, propValue in pairs(semanticConfig.fixedProperties) do
            if not skipProperties[propName] then -- Check skipProperties as well
                local validatedValue = validatePropertyValue(propValue)
                if validatedValue and validatedValue ~= '' then
                    -- Pass as a single-item array to be processed by the
                    -- 'Direct array of values' path in SemanticAnnotations.lua
                    finalProperties[propName] = {validatedValue}  
                end
            end
        end
    end
   
    -- Add debug info as HTML comment (Phase 1 monitoring)
    local basicCount = 0
    for _ in pairs(allProperties) do basicCount = basicCount + 1 end
   
    local debugInfo = string.format(
        "<!-- SMW Debug: basic_props=%d, additional_props=%d, unique_signatures=%d -->",
        basicCount,
         collector.count or 0,
        table.maxn(collector.seen or {})
     )
     )
     local countryKey = cr.semanticProperties.country
      
     local regionKey = cr.semanticProperties.region
     -- Send all properties to SemanticAnnotations in one batch
    allProperties[countryKey] = norm[countryKey]
    allProperties[regionKey]  = norm[regionKey]
     local semanticOutput = SemanticAnnotations.setSemanticProperties(
     local semanticOutput = SemanticAnnotations.setSemanticProperties(
         args,
         args,
         allProperties,
         finalProperties,
         semanticOptions
         semanticOptions
     )
     )
      
      
     return semanticOutput
     -- Append debug info to output
    if semanticOutput and semanticOutput ~= '' then
        return semanticOutput .. '\n' .. debugInfo
    else
        return debugInfo
    end
end
end


Line 1,203: Line 1,167:
-- @return string The rendered template HTML
-- @return string The rendered template HTML
function p.renderTemplate(template, frame)
function p.renderTemplate(template, frame)
    template.current_frame = frame -- Store frame on template instance
   
    -- Generate a unique ID for the title element for ARIA
    local pageId = TemplateHelpers.getCurrentPageId() or '0'
    template.titleId = 'template-title-' .. template.type .. '-' .. pageId
    -- 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
        return '<span class="error">Template recursion depth exceeded (limit: 3)</span>'
    end
   
     if not template._errorContext then
     if not template._errorContext then
         template._errorContext = p.createErrorContext(template)
         template._errorContext = p.createErrorContext(template)
     end
     end
   
    ErrorHandling.addStatus(template._errorContext, "LuaTemplateBlueprint", "Now rendering " .. template.type)
      
      
     if not template.config.meta then
     if not template.config.meta then
Line 1,213: Line 1,197:
     local args = frame:getParent().args or {}
     local args = frame:getParent().args or {}
     args = TemplateHelpers.normalizeArgumentCase(args)
     args = TemplateHelpers.normalizeArgumentCase(args)
   
    -- Increment recursion depth for any child template calls
    args._recursion_depth = tostring(depth + 1)
      
      
     args = p.runPreprocessors(template, args)
     args = p.runPreprocessors(template, args)
Line 1,224: Line 1,211:
         tableClass = tableClass,
         tableClass = tableClass,
         blocks = {},
         blocks = {},
         containerTag = template.features.fullPage and "div" or "table"
         containerTag = template.features.fullPage and "div" or "table",
        ariaLabelledBy = template.titleId
     }
     }
      
      
Line 1,239: Line 1,227:
     end
     end
      
      
     return TemplateStructure.render(args, structureConfig, template._errorContext)
     local result = TemplateStructure.render(args, structureConfig, template._errorContext)
   
    -- Append status and error divs to the final output
    result = result .. ErrorHandling.formatCombinedOutput(template._errorContext)
   
    template.current_frame = nil -- Clear frame from template instance
   
    return result
end
end