Jump to content

Module:SemanticAnnotations: Difference between revisions

// via Wikitext Extension for VSCode
// via Wikitext Extension for VSCode
Line 4: Line 4:
local p = {}
local p = {}
local NormalizationText = require('Module:NormalizationText')
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


-- Trim whitespace helper
-- Trim whitespace helper
Line 106: Line 110:
     }
     }
     return p.generateAnnotations(parentArgs, mappings, options)
     return p.generateAnnotations(parentArgs, mappings, options)
end
-- Prune properties to prevent server crashes
local function prune(properties)
    local total = 0
    for prop,val in pairs(properties) do
        if type(val)=='table' then
            -- dedup array
            local seen, compact = {}, {}
            for _,v in ipairs(val) do if not seen[v] then seen[v]=true; compact[#compact+1]=v end end
            properties[prop] = compact
        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
end


Line 349: Line 374:
             end
             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
     end
      
      
     -- Set properties if any exist
     -- Set properties if any exist
     if next(properties) then
     if next(pruned) then
         local success, result = pcall(function() return mw.smw.set(properties) end)
         local success, result = pcall(function() return mw.smw.set(pruned) end)
         if success then return semanticOutput
         if success then return semanticOutput
         else return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput
         else return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput

Revision as of 14:19, 24 April 2025

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

-- Module:SemanticAnnotations
-- Generates semantic annotations for templates in MediaWiki

local p = {}
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

-- Trim whitespace helper
-- Now delegates to NormalizationText
local function trim(s)
    return NormalizationText.trim(s)
end

--[[ Generates semantic annotations using #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 = args[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
        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 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

-- Renders a table using TemplateStructure and adds annotations
function p.renderWithSemantics(args, config, semanticMappings, semanticOptions)
    local TemplateStructure = require('Module:TemplateStructure')
    local renderedTable = TemplateStructure.render(args, config)
    local annotations = p.generateAnnotations(args, semanticMappings, semanticOptions)
    return renderedTable .. "\n" .. annotations
end

-- Allows templates to append annotations directly via transclusion
function p.appendToTemplate(frame)
    local args = frame.args
    local parent = frame:getParent()
    local parentArgs = parent and parent.args or {}
    
    -- Build mappings from numbered 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 and generate annotations
    local options = {
        visible = args.visible == "true",
        prefix = args.prefix or ""
    }
    return p.generateAnnotations(parentArgs, mappings, options)
end

-- Prune properties to prevent server crashes
local function prune(properties)
    local total = 0
    for prop,val in pairs(properties) do
        if type(val)=='table' then
            -- dedup array
            local seen, compact = {}, {}
            for _,v in ipairs(val) do if not seen[v] then seen[v]=true; compact[#compact+1]=v end end
            properties[prop] = compact
        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 {}
        local value = args[param]
        
        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 version supporting complex mappings (fallback when mw.smw unavailable)
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
            propertyCount = propertyCount + addSimplePropertyToResult(result, 
                fullPropertyName, args[mapping], transform[property], default[property])
        elseif type(mapping) == "table" then
            if mapping.param then
                -- Object with param 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]
                    
                    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 with native API or fallback
  @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
            processSimpleMapping(properties, fullPropertyName, args[mapping], 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
                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
    
    -- 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

-- For backward compatibility
generateSmwSubobjectFragment = p.generateSmwSubobjectFragment

return p