Jump to content

Module:SemanticAnnotations: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
 
(38 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- Module:SemanticAnnotations
--[[
-- Generates semantic annotations for templates
* Name: SemanticAnnotations
-- Compatible with Semantic MediaWiki, Semantic Scribunto, Semantic Drilldown, and DynamicPageList3
* Author: Mark W. Datysgeld
-- Docs: https://github.com/SemanticMediaWiki/SemanticScribunto/tree/master/docs
* Description: Primary semantic integration module for generating semantic properties with transformation support and property limits
* Notes: Implements batching, deduplication and property limits (200 total, 25 per property); supports simple, object, complex, and subobject mappings; falls back to parser functions when mw.smw unavailable; includes pruning to prevent server crashes
]]


local p = {}
local p = {}
local TemplateHelpers = require('Module:TemplateHelpers')
local NormalizationText = require('Module:NormalizationText')


-- Private helper function to trim whitespace
-- Limits to prevent server crashes
local function trim(s)
local TOTAL_LIMIT = 200  -- per page render, tune later
    return (s:gsub("^%s+", ""):gsub("%s+$", ""))
local VALUE_LIMIT = 25    -- per individual property
end


--[[
--[[ Fallback for mw.smw.set using the #set parser function.
    Generates semantic annotations using SMW's #set parser function.
  @param args - Template parameters
    This function now handles both legacy string mappings and new complex mappings.
  @param mappings - Property mappings: {["Property"] = "param"} or complex format
   
  @param options - Config options (visible, prefix, transform, default, conditional)
    @param args     - Template parameters table
  @return Wikitext with semantic annotations
    @param mappings - Property to parameter mappings in any of these formats:
                      1. Simple string: {["Property"] = "param_name"}
                      2. Object with param: {["Property"] = {param = "param_name"}}
                      3. Object with multiple mappings: {["Property"] = {mappings = [{param = "p1", metadata = {...}}, ...]}}
    @param options - Configuration options:
                    {
                      visible = false,   -- Show annotations (default: false)
                      prefix = "",       -- Property prefix
                      transform = {},   -- Transform functions: {["Property"] = function(val) ... end}
                      default = {},     -- Default values: {["Property"] = "default"}
                      conditional = {},  -- Conditional mappings: {["Property"] = {param="x", value="y"}}
                    }
   
    @return Wikitext string containing semantic annotations
]]
]]
function p.generateAnnotations(args, mappings, options)
function p.generateAnnotations(args, mappings, options)
     -- For complex mappings, just delegate to generateEnhancedAnnotations
     -- If complex mappings found, delegate to enhanced function
     if mappings and type(mappings) == "table" then
     if mappings and type(mappings) == "table" then
         for _, mapping in pairs(mappings) do
         for _, mapping in pairs(mappings) do
             if type(mapping) == "table" then
             if type(mapping) == "table" then return p.generateEnhancedAnnotations(args, mappings, options) end
                -- Found at least one complex mapping, use the enhanced function
                return p.generateEnhancedAnnotations(args, mappings, options)
            end
         end
         end
     end
     end
      
      
     -- If we got here, all mappings are simple string mappings
     -- Set defaults
     args = args or {}
     args = args or {}
     mappings = mappings or {}
     mappings = mappings or {}
     options = options or {}
     options = options or {}
   
    -- Set defaults for options
     local visible = options.visible or false
     local visible = options.visible or false
     local prefix = options.prefix or ""
     local prefix = options.prefix or ""
Line 54: Line 38:
     local conditional = options.conditional or {}
     local conditional = options.conditional or {}
      
      
     -- Start building the annotation block
     -- Build annotation block
     local result = {}
     local result = {}
   
     if not visible then table.insert(result, '<div style="display:none;">') end
    -- Determine if we need the hidden div wrapper
     if not visible then
        table.insert(result, '<div style="display:none;">')
    end
   
    -- Start the #set parser function
     table.insert(result, '  {{#set:')
     table.insert(result, '  {{#set:')
   
    -- Process all property mappings
     local propertyCount = 0
     local propertyCount = 0
      
      
     -- Handle regular property mappings (legacy string-to-string format)
     -- Process string mappings
     for property, param in pairs(mappings) do
     for property, param in pairs(mappings) do
        -- Only process string params (skip tables which are handled by enhanced function)
         if type(param) == "string" then
         if type(param) == "string" then
             local fullPropertyName = prefix .. property
             local fullPropertyName = prefix .. property
             local value = args[param]
             local _, value = TemplateHelpers.getFieldValue(args, { key = param })
              
              
             -- Apply transform if one exists for this property
             -- Apply transform if needed
             if value and transform[property] then
             if value and transform[property] then value = transform[property](value) end
                value = transform[property](value)
            end
              
              
             -- Use the value if it exists, otherwise use default if provided
             -- Add property if value exists or default provided
             if value and value ~= "" then
             if value and value ~= "" then
                 table.insert(result, string.format('    |%s=%s', fullPropertyName, value))
                 table.insert(result, string.format('    |%s=%s', fullPropertyName, value))
Line 89: Line 62:
             end
             end
         end
         end
        -- No need for goto continue - just continue the loop naturally
     end
     end
      
      
     -- Handle conditional properties
     -- Process conditional properties
     for property, condition in pairs(conditional) do
     for property, condition in pairs(conditional) do
         local fullPropertyName = prefix .. property
         local fullPropertyName = prefix .. property
         if args[condition.param] and args[condition.param] == condition.value then
         local _, condValue = TemplateHelpers.getFieldValue(args, { key = condition.param })
        if condValue == condition.value then
             table.insert(result, string.format('    |%s=%s', fullPropertyName, condition.target or "true"))
             table.insert(result, string.format('    |%s=%s', fullPropertyName, condition.target or "true"))
             propertyCount = propertyCount + 1
             propertyCount = propertyCount + 1
Line 101: Line 74:
     end
     end
      
      
     -- Close the #set parser function
     -- Close the parser function and wrapper
     table.insert(result, '  }}')
     table.insert(result, '  }}')
    if not visible then table.insert(result, '</div>') end
      
      
     -- Close the hidden div if we're using it
     -- Return result or empty string
    if not visible then
     return propertyCount > 0 and table.concat(result, "\n") or ""
        table.insert(result, '</div>')
    end
   
    -- If no properties were set, return an empty string
     if propertyCount == 0 then
        return ""
    end
   
    -- Join all lines and return
    return table.concat(result, "\n")
end
 
--[[
    Renders a table using TemplateStructure and adds semantic annotations.
   
    @param args            - Template parameters
    @param config          - TemplateStructure configuration
    @param semanticMappings - Property to parameter mappings
    @param semanticOptions  - Annotation configuration options
   
    @return Rendered template with semantic annotations
]]
function p.renderWithSemantics(args, config, semanticMappings, semanticOptions)
    local TemplateStructure = require('Module:TemplateStructure')
   
    -- Render the table structure
    local renderedTable = TemplateStructure.render(args, config)
   
    -- Generate the semantic annotations
    local annotations = p.generateAnnotations(args, semanticMappings, semanticOptions)
   
    -- Combine and return
    return renderedTable .. "\n" .. annotations
end
end


-- Allows templates to append semantic annotations directly via transclusion
-- Prune properties to prevent server crashes
function p.appendToTemplate(frame)
local function prune(properties)
     local args = frame.args
     local total = 0
     local parent = frame:getParent()
     local TemplateHelpers = require('Module:TemplateHelpers')
    local parentArgs = parent and parent.args or {}
      
      
     -- Mapping is defined as pairs of properties and parameters
     for prop,val in pairs(properties) do
    local mappings = {}
        if type(val)=='table' then
    local i = 1
            -- dedup array using centralized removeDuplicates function
   
            properties[prop] = TemplateHelpers.removeDuplicates(val)
    while args["property" .. i] and args["param" .. i] do
        end
        mappings[args["property" .. i]] = args["param" .. i]
        -- per-property cap
         i = i + 1
        if type(properties[prop])=='table' and #properties[prop] > VALUE_LIMIT then
            properties[prop] = { unpack(properties[prop],1,VALUE_LIMIT) }
         end
        -- global counter
        total = total + (type(properties[prop])=='table' and #properties[prop] or 1)
     end
     end
      
     if total > TOTAL_LIMIT then return nil, "SMW limit hit: "..total end
    -- Extract options
     return properties
    local options = {
        visible = args.visible == "true",
        prefix = args.prefix or ""
    }
   
     -- Generate and return the annotations
    return p.generateAnnotations(parentArgs, mappings, options)
end
end


-- Helper function for processing a simple property mapping
-- Process simple property mapping
local function processSimpleMapping(properties, propertyName, value, transformFunc, defaultValue)
local function processSimpleMapping(properties, propertyName, value, transformFunc, defaultValue)
     -- Apply transform if one exists and we have a value
     -- Apply transform if applicable
     if value and value ~= "" and transformFunc then
     if value and value ~= "" and transformFunc then value = transformFunc(value) end
        value = transformFunc(value)
    end
      
      
     -- Use value if it exists, otherwise use default
     -- Handle value setting with array conversion if needed
     if value and value ~= "" then
     if value and value ~= "" then
        -- If property already exists, convert to array or append
         if properties[propertyName] then
         if properties[propertyName] then
             -- Convert to array if it's the first duplicate
             -- Convert to array if first duplicate
             if type(properties[propertyName]) ~= "table" then
             if type(properties[propertyName]) ~= "table" then
                 properties[propertyName] = {properties[propertyName]}
                 properties[propertyName] = {properties[propertyName]}
             end
             end
            -- Append new value
             table.insert(properties[propertyName], value)
             table.insert(properties[propertyName], value)
         else
         else
            -- First value for this property
             properties[propertyName] = value
             properties[propertyName] = value
         end
         end
Line 192: Line 124:
end
end


-- Helper function for processing a complex property mapping with metadata
-- Process complex property mapping with metadata
local function processComplexMapping(properties, propertyName, args, mappings, transformFunc)
local function processComplexMapping(properties, propertyName, args, mappings, transformFunc)
     for _, mappingEntry in ipairs(mappings) do
     for _, mappingEntry in ipairs(mappings) do
         local param = mappingEntry.param
         local param = mappingEntry.param
         local metadata = mappingEntry.metadata or {}
         local metadata = mappingEntry.metadata or {}
         local value = args[param]
        -- Case-insensitive lookup: try exact match first, then lowercase
         local value = args[param] or args[param:lower()]
          
          
        -- Process only if value exists
         if value and value ~= "" then
         if value and value ~= "" then
             -- Apply transform if available
             -- Apply transform
             if transformFunc then
             if transformFunc then value = transformFunc(value) end
                value = transformFunc(value)
            end
              
              
             -- Add metadata qualifiers to property name if metadata exists
             -- Handle metadata qualifiers
             local qualifiedProperty = propertyName
             local qualifiedProperty = propertyName
             if next(metadata) then
             if next(metadata) then
Line 213: Line 143:
                     table.insert(qualifiers, metaKey .. "=" .. metaValue)
                     table.insert(qualifiers, metaKey .. "=" .. metaValue)
                 end
                 end
                -- Sort for consistency
                 table.sort(qualifiers)
                 table.sort(qualifiers)
                 qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";")
                 qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";")
             end
             end
              
              
             -- Set the property with qualified name
             -- Set property with array handling
             if properties[qualifiedProperty] then
             if properties[qualifiedProperty] then
                -- Convert to array if it's the first duplicate
                 if type(properties[qualifiedProperty]) ~= "table" then
                 if type(properties[qualifiedProperty]) ~= "table" then
                     properties[qualifiedProperty] = {properties[qualifiedProperty]}
                     properties[qualifiedProperty] = {properties[qualifiedProperty]}
                 end
                 end
                -- Append new value
                 table.insert(properties[qualifiedProperty], value)
                 table.insert(properties[qualifiedProperty], value)
             else
             else
                -- First value for this property
                 properties[qualifiedProperty] = value
                 properties[qualifiedProperty] = value
             end
             end
         end
         end
        -- No need for goto continue - just continue the loop naturally
     end
     end
end
end


-- Helper function for adding a simple property to parser function result
-- Add simple property to parser function result
local function addSimplePropertyToResult(result, propertyName, value, transformFunc, defaultValue)
local function addSimplePropertyToResult(result, propertyName, value, transformFunc, defaultValue)
    -- Apply transform if one exists and we have a value
     if value and value ~= "" and transformFunc then value = transformFunc(value) end
     if value and value ~= "" and transformFunc then
        value = transformFunc(value)
    end
      
      
    -- Use value if it exists, otherwise use default
     if value and value ~= "" then
     if value and value ~= "" then
         table.insert(result, string.format('    |%s=%s', propertyName, value))
         table.insert(result, string.format('    |%s=%s', propertyName, value))
Line 250: Line 171:
         return 1
         return 1
     end
     end
   
     return 0
     return 0
end
end


--[[
-- Enhanced fallback for mw.smw.set with complex mapping support.
    Enhanced version of generateAnnotations that supports complex mappings
    Used as fallback when mw.smw is not available
]]
function p.generateEnhancedAnnotations(args, mappings, options)
function p.generateEnhancedAnnotations(args, mappings, options)
     args = args or {}
     args = args or {}
Line 263: Line 180:
     options = options or {}
     options = options or {}
      
      
     -- Set defaults for options
     -- Initialize with defaults
     local visible = options.visible or false
     local visible = options.visible or false
     local prefix = options.prefix or ""
     local prefix = options.prefix or ""
Line 270: Line 187:
     local conditional = options.conditional or {}
     local conditional = options.conditional or {}
      
      
     -- Start building the annotation block
     -- Build annotation block
     local result = {}
     local result = {}
   
     if not visible then table.insert(result, '<div style="display:none;">') end
    -- Determine if we need the hidden div wrapper
     if not visible then
        table.insert(result, '<div style="display:none;">')
    end
   
    -- Start the #set parser function
     table.insert(result, '  {{#set:')
     table.insert(result, '  {{#set:')
   
    -- Process all property mappings
     local propertyCount = 0
     local propertyCount = 0
      
      
     -- Generate property sets for parser function
     -- Process all property types
     for property, mapping in pairs(mappings) do
     for property, mapping in pairs(mappings) do
         local fullPropertyName = prefix .. property
         local fullPropertyName = prefix .. property
          
          
        -- Handle different mapping types
         if type(mapping) == "string" then
         if type(mapping) == "string" then
             -- Legacy simple string mapping
             -- Simple string mapping with case-insensitive lookup
            local value = args[mapping] or args[mapping:lower()]
             propertyCount = propertyCount + addSimplePropertyToResult(result,  
             propertyCount = propertyCount + addSimplePropertyToResult(result,  
                 fullPropertyName, args[mapping], transform[property], default[property])
                 fullPropertyName, value, transform[property], default[property])
         elseif type(mapping) == "table" then
         elseif type(mapping) == "table" then
             if mapping.param then
             if mapping.param then
                 -- Single mapping with object structure
                 -- Object with param structure with case-insensitive lookup
                local value = args[mapping.param] or args[mapping.param:lower()]
                 propertyCount = propertyCount + addSimplePropertyToResult(result,  
                 propertyCount = propertyCount + addSimplePropertyToResult(result,  
                     fullPropertyName, args[mapping.param], transform[property], default[property])
                     fullPropertyName, value, transform[property], default[property])
             elseif mapping.mappings then
             elseif mapping.mappings then
                 -- Complex mapping with multiple parameters
                 -- Complex mapping with multiple parameters
Line 303: Line 213:
                     local param = mappingEntry.param
                     local param = mappingEntry.param
                     local metadata = mappingEntry.metadata or {}
                     local metadata = mappingEntry.metadata or {}
                     local value = args[param]
                    -- Case-insensitive lookup
                     local value = args[param] or args[param:lower()]
                      
                      
                    -- Process only if value exists
                     if value and value ~= "" then
                     if value and value ~= "" then
                         -- Apply transform if available
                         -- Apply transform
                         if transform[property] then
                         if transform[property] then value = transform[property](value) end
                            value = transform[property](value)
                        end
                          
                          
                         -- Add metadata qualifiers to property name if metadata exists
                         -- Add metadata qualifiers
                         local qualifiedProperty = fullPropertyName
                         local qualifiedProperty = fullPropertyName
                         if next(metadata) then
                         if next(metadata) then
Line 319: Line 227:
                                 table.insert(qualifiers, metaKey .. "=" .. metaValue)
                                 table.insert(qualifiers, metaKey .. "=" .. metaValue)
                             end
                             end
                            -- Sort for consistency
                             table.sort(qualifiers)
                             table.sort(qualifiers)
                             qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";")
                             qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";")
                         end
                         end
                          
                          
                         -- Add the property to result
                         -- Add property to result
                         table.insert(result, string.format('    |%s=%s', qualifiedProperty, value))
                         table.insert(result, string.format('    |%s=%s', qualifiedProperty, value))
                         propertyCount = propertyCount + 1
                         propertyCount = propertyCount + 1
                     end
                     end
                    -- No need for goto continue - just continue the loop naturally
                 end
                 end
             end
             end
Line 334: Line 240:
     end
     end
      
      
     -- Handle conditional properties (maintain backwards compatibility)
     -- Handle conditional properties
     for property, condition in pairs(conditional) do
     for property, condition in pairs(conditional) do
         local fullPropertyName = prefix .. property
         local fullPropertyName = prefix .. property
Line 343: Line 249:
     end
     end
      
      
     -- Close the #set parser function
     -- Close parser function and wrapper
     table.insert(result, '  }}')
     table.insert(result, '  }}')
    if not visible then table.insert(result, '</div>') end
      
      
     -- Close the hidden div if we're using it
     -- Return result or empty string
    if not visible then
     return propertyCount > 0 and table.concat(result, "\n") or ""
        table.insert(result, '</div>')
    end
   
    -- If no properties were set, return an empty string
     if propertyCount == 0 then
        return ""
    end
   
    -- Join all lines and return
    return table.concat(result, "\n")
end
end


--[[
--[[ Sets semantic properties using the native mw.smw API.
    Enhanced version of setSemanticProperties that supports complex mappings
  This is the primary function for setting semantic data.
   
  @param args - Template parameters
    @param args     - Template parameters
  @param mappings - Property mappings in formats:
    @param mappings - Property to parameter mappings with possible metadata
    - Simple: {["Property"] = "param_name"}
                      Format can be:
    - Object: {["Property"] = {param = "param_name"}}
                      1. Simple: {["Property"] = "param_name"}
    - Complex: {["Property"] = {mappings = [{param="p1", metadata={...}}, ...]}}
                      2. Object: {["Property"] = {param = "param_name"}}
    - Subobject: {["Property"] = {is_subobject=true, properties={...}, id_prefix="..."}}
                      3. Complex: {["Property"] = {mappings = {{param = "p1", metadata = {...}}, ...}}}
  @param options - Configuration options
    @param options - Configuration options (same as generateAnnotations)
  @return Generated markup or empty string if using native API
   
    @return Empty string if set via mw.smw, or generated annotations string
]]
]]
function p.setSemanticProperties(args, mappings, options)
function p.setSemanticProperties(args, mappings, options)
     -- Check if mw.smw is available
     -- Fall back to parser functions if mw.smw unavailable
     if not mw.smw then
     if not mw.smw then return p.generateEnhancedAnnotations(args, mappings, options) end
        -- Fall back to enhanced parser function approach
        return p.generateEnhancedAnnotations(args, mappings, options)
    end
      
      
     options = options or {}
     options = options or {}
Line 384: Line 276:
     local default = options.default or {}
     local default = options.default or {}
     local prefix = options.prefix or ""
     local prefix = options.prefix or ""
   
    -- Build the property table for mw.smw.set
     local properties = {}
     local properties = {}
    local semanticOutput = ""
      
      
     -- Process all mappings
     -- Process all mapping types
     for property, mapping in pairs(mappings) do
     for property, mapping in pairs(mappings) do
         local fullPropertyName = prefix .. property
         local fullPropertyName = prefix .. property
          
          
        -- Determine the type of mapping
         if type(mapping) == "string" then
         if type(mapping) == "string" then
             -- Legacy simple string mapping
             -- Simple string mapping with case-insensitive lookup
             processSimpleMapping(properties, fullPropertyName, args[mapping], transform[property], default[property])
            -- Try exact match first, then lowercase
            local value = args[mapping] or args[mapping:lower()]
             processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
         elseif type(mapping) == "table" then
         elseif type(mapping) == "table" then
             if mapping.param then
             if mapping.is_subobject then
                 -- Single mapping with object structure
                -- Subobject definition
                 processSimpleMapping(properties, fullPropertyName, args[mapping.param], transform[property], default[property])
                local subobjectProperties = mapping.properties or {}
                local actualProperties = {}
               
                -- Process subobject properties
                for subPropName, subPropValue in pairs(subobjectProperties) do
                    if type(subPropValue) == "table" and subPropValue.param then
                        -- Object with param reference
                        local paramName = subPropValue.param
                        if args[paramName] and args[paramName] ~= "" then
                            local value = args[paramName]
                            if subPropValue.transform and type(subPropValue.transform) == "function" then
                                value = subPropValue.transform(value)
                            end
                            actualProperties[subPropName] = value
                        end
                    elseif type(subPropValue) == "string" then
                        -- String mapping or static value
                        if args[subPropValue] and args[subPropValue] ~= "" then
                            actualProperties[subPropName] = args[subPropValue]
                        else
                            actualProperties[subPropName] = subPropValue
                        end
                    end
                end
               
                -- Create subobject if properties exist
                if next(actualProperties) then
                    -- Generate ID
                    local idPrefix = mapping.id_prefix or "subobj"
                    local idValue = ""
                    local primaryProp = mapping.primary_property
                   
                    if primaryProp and actualProperties[primaryProp] then
                        idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_")
                    else
                        idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999)
                    end
                   
                    local subobjectId = idPrefix .. "_" .. idValue
                   
                    -- Create subobject
                    local subobjectResult = mw.smw.subobject({
                        id = subobjectId,
                        properties = actualProperties
                    })
                   
                    -- Add error info if needed
                    if type(subobjectResult) == "table" and subobjectResult.error then
                        semanticOutput = semanticOutput .. "\n<!-- SMW Error: " ..
                                        tostring(subobjectResult.error) .. " -->"
                    end
                end
            elseif mapping.param then
                 -- Single mapping with object structure with case-insensitive lookup
                local value = args[mapping.param] or args[mapping.param:lower()]
                 processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
             elseif mapping.mappings then
             elseif mapping.mappings then
                 -- Complex mapping with multiple parameters
                 -- Complex mapping with multiple parameters
                 processComplexMapping(properties, fullPropertyName, args, mapping.mappings, transform[property])
                 processComplexMapping(properties, fullPropertyName, args, mapping.mappings, transform[property])
            else
                -- Direct array of values: add each value as a property
                for _, value in ipairs(mapping) do
                    processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
                end
             end
             end
         end
         end
     end
     end
      
      
     -- Only set properties if we have some
     -- Before setting properties, prune them
     if next(properties) then
     local pruned, err = prune(properties)
         -- Set the properties using the native SMW interface
    if not pruned then
         local success, result = pcall(function()
         -- Add HTML comment with error message
            return mw.smw.set(properties)
        semanticOutput = semanticOutput .. "\n<!-- " .. err .. " -->"
        end)
        return semanticOutput
          
    end
         -- If successful, return empty string (properties are set behind the scenes)
   
         -- If failed, fall back to parser function approach
    -- Set properties if any exist
        if success then
    if next(pruned) then
            return ""
         local success, result = pcall(function() return mw.smw.set(pruned) end)
         else
         if success then return semanticOutput
             return p.generateEnhancedAnnotations(args, mappings, options)
         else return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput
         end
    end
   
    return semanticOutput
end
 
-- Generate #subobject parser function with optional ID
function p.generateSmwSubobjectFragment(properties, id)
    local result = '<div style="display:none;">\n  {{#subobject:'
    if id and id ~= "" then result = result .. "|@" .. id end
   
    for propName, propValue in pairs(properties) do
         if propValue and propValue ~= "" then
             result = result .. "\n    |" .. propName .. "=" .. propValue
         end
         end
     end
     end
      
      
     return ""
     result = result .. "\n  }}\n</div>"
    return result
end
end


return p
return p

Latest revision as of 03:16, 25 August 2025

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

--[[
* Name: SemanticAnnotations
* Author: Mark W. Datysgeld
* Description: Primary semantic integration module for generating semantic properties with transformation support and property limits
* Notes: Implements batching, deduplication and property limits (200 total, 25 per property); supports simple, object, complex, and subobject mappings; falls back to parser functions when mw.smw unavailable; includes pruning to prevent server crashes
]]

local p = {}
local TemplateHelpers = require('Module:TemplateHelpers')
local NormalizationText = require('Module:NormalizationText')

-- Limits to prevent server crashes
local TOTAL_LIMIT = 200   -- per page render, tune later
local VALUE_LIMIT = 25    -- per individual property

--[[ Fallback for mw.smw.set using the #set parser function.
  @param args - Template parameters
  @param mappings - Property mappings: {["Property"] = "param"} or complex format
  @param options - Config options (visible, prefix, transform, default, conditional)
  @return Wikitext with semantic annotations
]]
function p.generateAnnotations(args, mappings, options)
    -- If complex mappings found, delegate to enhanced function
    if mappings and type(mappings) == "table" then
        for _, mapping in pairs(mappings) do
            if type(mapping) == "table" then return p.generateEnhancedAnnotations(args, mappings, options) end
        end
    end
    
    -- Set defaults
    args = args or {}
    mappings = mappings or {}
    options = options or {}
    local visible = options.visible or false
    local prefix = options.prefix or ""
    local transform = options.transform or {}
    local default = options.default or {}
    local conditional = options.conditional or {}
    
    -- Build annotation block
    local result = {}
    if not visible then table.insert(result, '<div style="display:none;">') end
    table.insert(result, '  {{#set:')
    local propertyCount = 0
    
    -- Process string mappings
    for property, param in pairs(mappings) do
        if type(param) == "string" then
            local fullPropertyName = prefix .. property
            local _, value = TemplateHelpers.getFieldValue(args, { key = param })
            
            -- Apply transform if needed
            if value and transform[property] then value = transform[property](value) end
            
            -- Add property if value exists or default provided
            if value and value ~= "" then
                table.insert(result, string.format('    |%s=%s', fullPropertyName, value))
                propertyCount = propertyCount + 1
            elseif default[property] then
                table.insert(result, string.format('    |%s=%s', fullPropertyName, default[property]))
                propertyCount = propertyCount + 1
            end
        end
    end
    
    -- Process conditional properties
    for property, condition in pairs(conditional) do
        local fullPropertyName = prefix .. property
        local _, condValue = TemplateHelpers.getFieldValue(args, { key = condition.param })
        if condValue == condition.value then
            table.insert(result, string.format('    |%s=%s', fullPropertyName, condition.target or "true"))
            propertyCount = propertyCount + 1
        end
    end
    
    -- Close the parser function and wrapper
    table.insert(result, '  }}')
    if not visible then table.insert(result, '</div>') end
    
    -- Return result or empty string
    return propertyCount > 0 and table.concat(result, "\n") or ""
end

-- Prune properties to prevent server crashes
local function prune(properties)
    local total = 0
    local TemplateHelpers = require('Module:TemplateHelpers')
    
    for prop,val in pairs(properties) do
        if type(val)=='table' then
            -- dedup array using centralized removeDuplicates function
            properties[prop] = TemplateHelpers.removeDuplicates(val)
        end
        -- per-property cap
        if type(properties[prop])=='table' and #properties[prop] > VALUE_LIMIT then
            properties[prop] = { unpack(properties[prop],1,VALUE_LIMIT) }
        end
        -- global counter
        total = total + (type(properties[prop])=='table' and #properties[prop] or 1)
    end
    if total > TOTAL_LIMIT then return nil, "SMW limit hit: "..total end
    return properties
end

-- Process simple property mapping
local function processSimpleMapping(properties, propertyName, value, transformFunc, defaultValue)
    -- Apply transform if applicable
    if value and value ~= "" and transformFunc then value = transformFunc(value) end
    
    -- Handle value setting with array conversion if needed
    if value and value ~= "" then
        if properties[propertyName] then
            -- Convert to array if first duplicate
            if type(properties[propertyName]) ~= "table" then
                properties[propertyName] = {properties[propertyName]}
            end
            table.insert(properties[propertyName], value)
        else
            properties[propertyName] = value
        end
    elseif defaultValue then
        properties[propertyName] = defaultValue
    end
end

-- Process complex property mapping with metadata
local function processComplexMapping(properties, propertyName, args, mappings, transformFunc)
    for _, mappingEntry in ipairs(mappings) do
        local param = mappingEntry.param
        local metadata = mappingEntry.metadata or {}
        -- Case-insensitive lookup: try exact match first, then lowercase
        local value = args[param] or args[param:lower()]
        
        if value and value ~= "" then
            -- Apply transform
            if transformFunc then value = transformFunc(value) end
            
            -- Handle metadata qualifiers
            local qualifiedProperty = propertyName
            if next(metadata) then
                local qualifiers = {}
                for metaKey, metaValue in pairs(metadata) do
                    table.insert(qualifiers, metaKey .. "=" .. metaValue)
                end
                table.sort(qualifiers)
                qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";")
            end
            
            -- Set property with array handling
            if properties[qualifiedProperty] then
                if type(properties[qualifiedProperty]) ~= "table" then
                    properties[qualifiedProperty] = {properties[qualifiedProperty]}
                end
                table.insert(properties[qualifiedProperty], value)
            else
                properties[qualifiedProperty] = value
            end
        end
    end
end

-- Add simple property to parser function result
local function addSimplePropertyToResult(result, propertyName, value, transformFunc, defaultValue)
    if value and value ~= "" and transformFunc then value = transformFunc(value) end
    
    if value and value ~= "" then
        table.insert(result, string.format('    |%s=%s', propertyName, value))
        return 1
    elseif defaultValue then
        table.insert(result, string.format('    |%s=%s', propertyName, defaultValue))
        return 1
    end
    return 0
end

-- Enhanced fallback for mw.smw.set with complex mapping support.
function p.generateEnhancedAnnotations(args, mappings, options)
    args = args or {}
    mappings = mappings or {}
    options = options or {}
    
    -- Initialize with defaults
    local visible = options.visible or false
    local prefix = options.prefix or ""
    local transform = options.transform or {}
    local default = options.default or {}
    local conditional = options.conditional or {}
    
    -- Build annotation block
    local result = {}
    if not visible then table.insert(result, '<div style="display:none;">') end
    table.insert(result, '  {{#set:')
    local propertyCount = 0
    
    -- Process all property types
    for property, mapping in pairs(mappings) do
        local fullPropertyName = prefix .. property
        
        if type(mapping) == "string" then
            -- Simple string mapping with case-insensitive lookup
            local value = args[mapping] or args[mapping:lower()]
            propertyCount = propertyCount + addSimplePropertyToResult(result, 
                fullPropertyName, value, transform[property], default[property])
        elseif type(mapping) == "table" then
            if mapping.param then
                -- Object with param structure with case-insensitive lookup
                local value = args[mapping.param] or args[mapping.param:lower()]
                propertyCount = propertyCount + addSimplePropertyToResult(result, 
                    fullPropertyName, value, transform[property], default[property])
            elseif mapping.mappings then
                -- Complex mapping with multiple parameters
                for _, mappingEntry in ipairs(mapping.mappings) do
                    local param = mappingEntry.param
                    local metadata = mappingEntry.metadata or {}
                    -- Case-insensitive lookup
                    local value = args[param] or args[param:lower()]
                    
                    if value and value ~= "" then
                        -- Apply transform
                        if transform[property] then value = transform[property](value) end
                        
                        -- Add metadata qualifiers
                        local qualifiedProperty = fullPropertyName
                        if next(metadata) then
                            local qualifiers = {}
                            for metaKey, metaValue in pairs(metadata) do
                                table.insert(qualifiers, metaKey .. "=" .. metaValue)
                            end
                            table.sort(qualifiers)
                            qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";")
                        end
                        
                        -- Add property to result
                        table.insert(result, string.format('    |%s=%s', qualifiedProperty, value))
                        propertyCount = propertyCount + 1
                    end
                end
            end
        end
    end
    
    -- Handle conditional properties
    for property, condition in pairs(conditional) do
        local fullPropertyName = prefix .. property
        if args[condition.param] and args[condition.param] == condition.value then
            table.insert(result, string.format('    |%s=%s', fullPropertyName, condition.target or "true"))
            propertyCount = propertyCount + 1
        end
    end
    
    -- Close parser function and wrapper
    table.insert(result, '  }}')
    if not visible then table.insert(result, '</div>') end
    
    -- Return result or empty string
    return propertyCount > 0 and table.concat(result, "\n") or ""
end

--[[ Sets semantic properties using the native mw.smw API.
  This is the primary function for setting semantic data.
  @param args - Template parameters
  @param mappings - Property mappings in formats:
     - Simple: {["Property"] = "param_name"}
     - Object: {["Property"] = {param = "param_name"}}
     - Complex: {["Property"] = {mappings = [{param="p1", metadata={...}}, ...]}}
     - Subobject: {["Property"] = {is_subobject=true, properties={...}, id_prefix="..."}}
  @param options - Configuration options
  @return Generated markup or empty string if using native API
]]
function p.setSemanticProperties(args, mappings, options)
    -- Fall back to parser functions if mw.smw unavailable
    if not mw.smw then return p.generateEnhancedAnnotations(args, mappings, options) end
    
    options = options or {}
    local transform = options.transform or {}
    local default = options.default or {}
    local prefix = options.prefix or ""
    local properties = {}
    local semanticOutput = ""
    
    -- Process all mapping types
    for property, mapping in pairs(mappings) do
        local fullPropertyName = prefix .. property
        
        if type(mapping) == "string" then
            -- Simple string mapping with case-insensitive lookup
            -- Try exact match first, then lowercase
            local value = args[mapping] or args[mapping:lower()]
            processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
        elseif type(mapping) == "table" then
            if mapping.is_subobject then
                -- Subobject definition
                local subobjectProperties = mapping.properties or {}
                local actualProperties = {}
                
                -- Process subobject properties
                for subPropName, subPropValue in pairs(subobjectProperties) do
                    if type(subPropValue) == "table" and subPropValue.param then
                        -- Object with param reference
                        local paramName = subPropValue.param
                        if args[paramName] and args[paramName] ~= "" then
                            local value = args[paramName]
                            if subPropValue.transform and type(subPropValue.transform) == "function" then
                                value = subPropValue.transform(value)
                            end
                            actualProperties[subPropName] = value
                        end
                    elseif type(subPropValue) == "string" then
                        -- String mapping or static value
                        if args[subPropValue] and args[subPropValue] ~= "" then
                            actualProperties[subPropName] = args[subPropValue]
                        else
                            actualProperties[subPropName] = subPropValue
                        end
                    end
                end
                
                -- Create subobject if properties exist
                if next(actualProperties) then
                    -- Generate ID
                    local idPrefix = mapping.id_prefix or "subobj"
                    local idValue = ""
                    local primaryProp = mapping.primary_property
                    
                    if primaryProp and actualProperties[primaryProp] then
                        idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_")
                    else
                        idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999)
                    end
                    
                    local subobjectId = idPrefix .. "_" .. idValue
                    
                    -- Create subobject
                    local subobjectResult = mw.smw.subobject({
                        id = subobjectId,
                        properties = actualProperties
                    })
                    
                    -- Add error info if needed
                    if type(subobjectResult) == "table" and subobjectResult.error then
                        semanticOutput = semanticOutput .. "\n<!-- SMW Error: " .. 
                                        tostring(subobjectResult.error) .. " -->"
                    end
                end
            elseif mapping.param then
                -- Single mapping with object structure with case-insensitive lookup
                local value = args[mapping.param] or args[mapping.param:lower()]
                processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
            elseif mapping.mappings then
                -- Complex mapping with multiple parameters
                processComplexMapping(properties, fullPropertyName, args, mapping.mappings, transform[property])
            else
                -- Direct array of values: add each value as a property
                for _, value in ipairs(mapping) do
                    processSimpleMapping(properties, fullPropertyName, value, transform[property], default[property])
                end
            end
        end
    end
    
    -- Before setting properties, prune them
    local pruned, err = prune(properties)
    if not pruned then
        -- Add HTML comment with error message
        semanticOutput = semanticOutput .. "\n<!-- " .. err .. " -->"
        return semanticOutput
    end
    
    -- Set properties if any exist
    if next(pruned) then
        local success, result = pcall(function() return mw.smw.set(pruned) end)
        if success then return semanticOutput
        else return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput
        end
    end
    
    return semanticOutput
end

-- Generate #subobject parser function with optional ID
function p.generateSmwSubobjectFragment(properties, id)
    local result = '<div style="display:none;">\n  {{#subobject:'
    if id and id ~= "" then result = result .. "|@" .. id end
    
    for propName, propValue in pairs(properties) do
        if propValue and propValue ~= "" then
            result = result .. "\n    |" .. propName .. "=" .. propValue
        end
    end
    
    result = result .. "\n  }}\n</div>"
    return result
end

return p