Jump to content

Module:SemanticAnnotations: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 368: Line 368:
                       2. Object: {["Property"] = {param = "param_name"}}
                       2. Object: {["Property"] = {param = "param_name"}}
                       3. Complex: {["Property"] = {mappings = {{param = "p1", metadata = {...}}, ...}}}
                       3. Complex: {["Property"] = {mappings = {{param = "p1", metadata = {...}}, ...}}}
                      4. Subobject: {["Property"] = {is_subobject = true, properties = {property map}, id_prefix = "optional_prefix"}}
     @param options  - Configuration options (same as generateAnnotations)
     @param options  - Configuration options (same as generateAnnotations)
      
      
Line 386: Line 387:
     -- Build the property table for mw.smw.set
     -- Build the property table for mw.smw.set
     local properties = {}
     local properties = {}
    local semanticOutput = ""
      
      
     -- Process all mappings
     -- Process all mappings
Line 396: Line 398:
             processSimpleMapping(properties, fullPropertyName, args[mapping], transform[property], default[property])
             processSimpleMapping(properties, fullPropertyName, args[mapping], transform[property], default[property])
         elseif type(mapping) == "table" then
         elseif type(mapping) == "table" then
             if mapping.param then
             if mapping.is_subobject then
                -- This is a special subobject definition
               
                -- Get the subobject properties map
                local subobjectProperties = mapping.properties or {}
               
                -- Create the actual properties map by processing each property value
                local actualProperties = {}
               
                -- Process each property definition in the subobject
                for subPropName, subPropValue in pairs(subobjectProperties) do
                    if type(subPropValue) == "table" and subPropValue.param then
                        -- Object with param reference
                        local paramName = subPropValue.param
                       
                        -- Get the value from args
                        if args[paramName] and args[paramName] ~= "" then
                            local value = args[paramName]
                           
                            -- Apply transform if one exists for this property
                            if subPropValue.transform and type(subPropValue.transform) == "function" then
                                value = subPropValue.transform(value)
                            end
                           
                            -- Set the property
                            actualProperties[subPropName] = value
                        end
                    elseif type(subPropValue) == "string" then
                        -- Simple string mapping or static value
                        if args[subPropValue] and args[subPropValue] ~= "" then
                            -- It's a parameter reference
                            actualProperties[subPropName] = args[subPropValue]
                        else
                            -- It's a static value
                            actualProperties[subPropName] = subPropValue
                        end
                    end
                end
               
                -- If we have at least one property to set
                if next(actualProperties) then
                    -- Generate a reasonably unique ID for the subobject
                    local idPrefix = mapping.id_prefix or "subobj"
                    local idValue = ""
                   
                    -- Try to use primary property value as part of the ID
                    local primaryProp = mapping.primary_property
                    if primaryProp and actualProperties[primaryProp] then
                        -- Clean the value to use in an ID
                        idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_")
                    else
                        -- Just use an incremental number if no primary property
                        idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999)
                    end
                   
                    -- Create the full ID
                    local subobjectId = idPrefix .. "_" .. idValue
                   
                    -- Create the subobject
                    if mw.smw then
                        -- Use native SMW API
                        local subobjectResult = mw.smw.subobject({
                            id = subobjectId,      -- Use our generated ID
                            properties = actualProperties
                        })
                       
                        -- If there was an error, append it to output for debugging
                        if type(subobjectResult) == "table" and subobjectResult.error then
                            semanticOutput = semanticOutput .. "\n<!-- SMW Subobject Error: " ..
                                            tostring(subobjectResult.error) .. " -->"
                        end
                    else
                        -- Parser function fallback
                        semanticOutput = semanticOutput .. "\n" ..
                                        generateSmwSubobjectFragment(actualProperties, subobjectId)
                    end
                end
            elseif mapping.param then
                 -- Single mapping with object structure
                 -- Single mapping with object structure
                 processSimpleMapping(properties, fullPropertyName, args[mapping.param], transform[property], default[property])
                 processSimpleMapping(properties, fullPropertyName, args[mapping.param], transform[property], default[property])
Line 413: Line 492:
         end)
         end)
          
          
         -- If successful, return empty string (properties are set behind the scenes)
         -- If successful, return semanticOutput (which might contain subobject results)
         -- If failed, fall back to parser function approach
         -- If failed, fall back to parser function approach
         if success then
         if success then
             return ""
             return semanticOutput
         else
         else
             return p.generateEnhancedAnnotations(args, mappings, options)
             return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput
         end
         end
     end
     end
      
      
     return ""
     return semanticOutput
end
end
-- Helper function to generate #subobject parser function with an ID
function p.generateSmwSubobjectFragment(properties, id)
    local result = '<div style="display:none;">\n  {{#subobject:'
   
    -- Set the ID if available
    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
-- For backward compatibility with local reference
generateSmwSubobjectFragment = p.generateSmwSubobjectFragment


return p
return p

Revision as of 03:27, 31 March 2025

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

-- Module:SemanticAnnotations
-- Generates semantic annotations for templates
-- Compatible with Semantic MediaWiki, Semantic Scribunto, Semantic Drilldown, and DynamicPageList3
-- Docs: https://github.com/SemanticMediaWiki/SemanticScribunto/tree/master/docs

local p = {}

-- Private helper function to trim whitespace
local function trim(s)
    return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

--[[
    Generates semantic annotations using SMW's #set parser function.
    This function now handles both legacy string mappings and new complex mappings.
    
    @param args     - Template parameters table
    @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)
    -- For complex mappings, just delegate to generateEnhancedAnnotations
    if mappings and type(mappings) == "table" then
        for _, mapping in pairs(mappings) do
            if type(mapping) == "table" then
                -- Found at least one complex mapping, use the enhanced function
                return p.generateEnhancedAnnotations(args, mappings, options)
            end
        end
    end
    
    -- If we got here, all mappings are simple string mappings
    args = args or {}
    mappings = mappings or {}
    options = options or {}
    
    -- Set defaults for options
    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 {}
    
    -- Start building the annotation block
    local result = {}
    
    -- 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:')
    
    -- Process all property mappings
    local propertyCount = 0
    
    -- Handle regular property mappings (legacy string-to-string format)
    for property, param in pairs(mappings) do
        -- Only process string params (skip tables which are handled by enhanced function)
        if type(param) == "string" then
            local fullPropertyName = prefix .. property
            local value = args[param]
            
            -- Apply transform if one exists for this property
            if value and transform[property] then
                value = transform[property](value)
            end
            
            -- Use the value if it exists, otherwise use default if 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
    
    -- 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 the #set parser function
    table.insert(result, '  }}')
    
    -- Close the hidden div if we're using it
    if not visible then
        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

-- Allows templates to append semantic annotations directly via transclusion
function p.appendToTemplate(frame)
    local args = frame.args
    local parent = frame:getParent()
    local parentArgs = parent and parent.args or {}
    
    -- Mapping is defined as pairs of properties and parameters
    local mappings = {}
    local i = 1
    
    while args["property" .. i] and args["param" .. i] do
        mappings[args["property" .. i]] = args["param" .. i]
        i = i + 1
    end
    
    -- Extract options
    local options = {
        visible = args.visible == "true",
        prefix = args.prefix or ""
    }
    
    -- Generate and return the annotations
    return p.generateAnnotations(parentArgs, mappings, options)
end

-- Helper function for processing a simple property mapping
local function processSimpleMapping(properties, 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
    
    -- Use value if it exists, otherwise use default
    if value and value ~= "" then
        -- If property already exists, convert to array or append
        if properties[propertyName] then
            -- Convert to array if it's the first duplicate
            if type(properties[propertyName]) ~= "table" then
                properties[propertyName] = {properties[propertyName]}
            end
            -- Append new value
            table.insert(properties[propertyName], value)
        else
            -- First value for this property
            properties[propertyName] = value
        end
    elseif defaultValue then
        properties[propertyName] = defaultValue
    end
end

-- Helper function for processing a 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 {}
        local value = args[param]
        
        -- Process only if value exists
        if value and value ~= "" then
            -- Apply transform if available - make sure we use the right scoped transform function
            if transformFunc then
                -- Apply the transformation
                value = transformFunc(value)
            end
            
            -- Add metadata qualifiers to property name if metadata exists
            local qualifiedProperty = propertyName
            if next(metadata) then
                local qualifiers = {}
                for metaKey, metaValue in pairs(metadata) do
                    table.insert(qualifiers, metaKey .. "=" .. metaValue)
                end
                -- Sort for consistency
                table.sort(qualifiers)
                qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";")
            end
            
            -- Set the property with qualified name
            if properties[qualifiedProperty] then
                -- Convert to array if it's the first duplicate
                if type(properties[qualifiedProperty]) ~= "table" then
                    properties[qualifiedProperty] = {properties[qualifiedProperty]}
                end
                -- Append new value
                table.insert(properties[qualifiedProperty], value)
            else
                -- First value for this property
                properties[qualifiedProperty] = value
            end
        end
        -- No need for goto continue - just continue the loop naturally
    end
end

-- Helper function for adding a simple property to parser function result
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
    
    -- Use value if it exists, otherwise use default
    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 version of generateAnnotations that supports complex mappings
    Used as fallback when mw.smw is not available
]]
function p.generateEnhancedAnnotations(args, mappings, options)
    args = args or {}
    mappings = mappings or {}
    options = options or {}
    
    -- Set defaults for options
    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 {}
    
    -- Start building the annotation block
    local result = {}
    
    -- 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:')
    
    -- Process all property mappings
    local propertyCount = 0
    
    -- Generate property sets for parser function
    for property, mapping in pairs(mappings) do
        local fullPropertyName = prefix .. property
        
        -- Handle different mapping types
        if type(mapping) == "string" then
            -- Legacy simple string mapping
            propertyCount = propertyCount + addSimplePropertyToResult(result, 
                fullPropertyName, args[mapping], transform[property], default[property])
        elseif type(mapping) == "table" then
            if mapping.param then
                -- Single mapping with object structure
                propertyCount = propertyCount + addSimplePropertyToResult(result, 
                    fullPropertyName, args[mapping.param], 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 {}
                    local value = args[param]
                    
                    -- Process only if value exists
                    if value and value ~= "" then
                        -- Apply transform if available
                        if transform[property] then
                            value = transform[property](value)
                        end
                        
                        -- Add metadata qualifiers to property name if metadata exists
                        local qualifiedProperty = fullPropertyName
                        if next(metadata) then
                            local qualifiers = {}
                            for metaKey, metaValue in pairs(metadata) do
                                table.insert(qualifiers, metaKey .. "=" .. metaValue)
                            end
                            -- Sort for consistency
                            table.sort(qualifiers)
                            qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";")
                        end
                        
                        -- Add the property to result
                        table.insert(result, string.format('    |%s=%s', qualifiedProperty, value))
                        propertyCount = propertyCount + 1
                    end
                end
            end
        end
    end
    
    -- Handle conditional properties (maintain backwards compatibility)
    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 the #set parser function
    table.insert(result, '  }}')
    
    -- Close the hidden div if we're using it
    if not visible then
        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

--[[
    Enhanced version of setSemanticProperties that supports complex mappings
    
    @param args     - Template parameters
    @param mappings - Property to parameter mappings with possible metadata
                      Format can be:
                      1. Simple: {["Property"] = "param_name"}
                      2. Object: {["Property"] = {param = "param_name"}}
                      3. Complex: {["Property"] = {mappings = {{param = "p1", metadata = {...}}, ...}}}
                      4. Subobject: {["Property"] = {is_subobject = true, properties = {property map}, id_prefix = "optional_prefix"}}
    @param options  - Configuration options (same as generateAnnotations)
    
    @return Empty string if set via mw.smw, or generated annotations string
]]
function p.setSemanticProperties(args, mappings, options)
    -- Check if mw.smw is available
    if not mw.smw then
        -- Fall back to enhanced parser function approach
        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 ""
    
    -- Build the property table for mw.smw.set
    local properties = {}
    local semanticOutput = ""
    
    -- Process all mappings
    for property, mapping in pairs(mappings) do
        local fullPropertyName = prefix .. property
        
        -- Determine the type of mapping
        if type(mapping) == "string" then
            -- Legacy simple string mapping
            processSimpleMapping(properties, fullPropertyName, args[mapping], transform[property], default[property])
        elseif type(mapping) == "table" then
            if mapping.is_subobject then
                -- This is a special subobject definition
                
                -- Get the subobject properties map
                local subobjectProperties = mapping.properties or {}
                
                -- Create the actual properties map by processing each property value
                local actualProperties = {}
                
                -- Process each property definition in the subobject
                for subPropName, subPropValue in pairs(subobjectProperties) do
                    if type(subPropValue) == "table" and subPropValue.param then
                        -- Object with param reference
                        local paramName = subPropValue.param
                        
                        -- Get the value from args
                        if args[paramName] and args[paramName] ~= "" then
                            local value = args[paramName]
                            
                            -- Apply transform if one exists for this property
                            if subPropValue.transform and type(subPropValue.transform) == "function" then
                                value = subPropValue.transform(value)
                            end
                            
                            -- Set the property
                            actualProperties[subPropName] = value
                        end
                    elseif type(subPropValue) == "string" then
                        -- Simple string mapping or static value
                        if args[subPropValue] and args[subPropValue] ~= "" then
                            -- It's a parameter reference
                            actualProperties[subPropName] = args[subPropValue]
                        else
                            -- It's a static value
                            actualProperties[subPropName] = subPropValue
                        end
                    end
                end
                
                -- If we have at least one property to set
                if next(actualProperties) then
                    -- Generate a reasonably unique ID for the subobject
                    local idPrefix = mapping.id_prefix or "subobj"
                    local idValue = ""
                    
                    -- Try to use primary property value as part of the ID
                    local primaryProp = mapping.primary_property
                    if primaryProp and actualProperties[primaryProp] then
                        -- Clean the value to use in an ID
                        idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_")
                    else
                        -- Just use an incremental number if no primary property
                        idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999)
                    end
                    
                    -- Create the full ID
                    local subobjectId = idPrefix .. "_" .. idValue
                    
                    -- Create the subobject
                    if mw.smw then
                        -- Use native SMW API
                        local subobjectResult = mw.smw.subobject({
                            id = subobjectId,       -- Use our generated ID
                            properties = actualProperties
                        })
                        
                        -- If there was an error, append it to output for debugging
                        if type(subobjectResult) == "table" and subobjectResult.error then
                            semanticOutput = semanticOutput .. "\n<!-- SMW Subobject Error: " .. 
                                            tostring(subobjectResult.error) .. " -->"
                        end
                    else
                        -- Parser function fallback
                        semanticOutput = semanticOutput .. "\n" .. 
                                        generateSmwSubobjectFragment(actualProperties, subobjectId)
                    end
                end
            elseif mapping.param then
                -- Single mapping with object structure
                processSimpleMapping(properties, fullPropertyName, args[mapping.param], transform[property], default[property])
            elseif mapping.mappings then
                -- Complex mapping with multiple parameters
                processComplexMapping(properties, fullPropertyName, args, mapping.mappings, transform[property])
            end
        end
    end
    
    -- Only set properties if we have some
    if next(properties) then
        -- Set the properties using the native SMW interface
        local success, result = pcall(function()
            return mw.smw.set(properties)
        end)
        
        -- If successful, return semanticOutput (which might contain subobject results)
        -- If failed, fall back to parser function approach
        if success then
            return semanticOutput
        else
            return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput
        end
    end
    
    return semanticOutput
end

-- Helper function to generate #subobject parser function with an ID
function p.generateSmwSubobjectFragment(properties, id)
    local result = '<div style="display:none;">\n  {{#subobject:'
    
    -- Set the ID if available
    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

-- For backward compatibility with local reference
generateSmwSubobjectFragment = p.generateSmwSubobjectFragment

return p