Module:SemanticAnnotations: Difference between revisions
Appearance
// 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 | -- 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