Module:SemanticAnnotations: Difference between revisions
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| (39 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
-- | --[[ | ||
* 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 p = {} | ||
local TemplateHelpers = require('Module:TemplateHelpers') | |||
local NormalizationText = require('Module:NormalizationText') | |||
-- | -- Limits to prevent server crashes | ||
local | 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) | 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 {} | args = args or {} | ||
mappings = mappings or {} | mappings = mappings or {} | ||
options = options or {} | options = options or {} | ||
local visible = options.visible or false | local visible = options.visible or false | ||
local prefix = options.prefix or "" | local prefix = options.prefix or "" | ||
| Line 39: | Line 38: | ||
local conditional = options.conditional or {} | local conditional = options.conditional or {} | ||
-- | -- Build annotation block | ||
local result = {} | local result = {} | ||
if not visible then table.insert(result, '<div style="display:none;">') end | |||
if not visible then | |||
table.insert(result, ' {{#set:') | table.insert(result, ' {{#set:') | ||
local propertyCount = 0 | local propertyCount = 0 | ||
-- | -- Process string mappings | ||
for property, param in pairs(mappings) do | for property, param in pairs(mappings) do | ||
local fullPropertyName = prefix .. property | 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 | ||
end | end | ||
-- | -- 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 | ||
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 82: | Line 74: | ||
end | end | ||
-- Close the | -- Close the parser function and wrapper | ||
table.insert(result, ' }}') | 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 | 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 | end | ||
if total > TOTAL_LIMIT then return nil, "SMW limit hit: "..total end | |||
return properties | |||
end | end | ||
-- | -- Process simple property mapping | ||
local function processSimpleMapping(properties, propertyName, value, transformFunc, defaultValue) | local function processSimpleMapping(properties, propertyName, value, transformFunc, defaultValue) | ||
-- Apply transform if | -- Apply transform if applicable | ||
if value and value ~= "" and transformFunc then | 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 value and value ~= "" then | ||
if properties[propertyName] then | if properties[propertyName] then | ||
-- Convert to array if | -- 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 | ||
table.insert(properties[propertyName], value) | table.insert(properties[propertyName], value) | ||
else | else | ||
properties[propertyName] = value | properties[propertyName] = value | ||
end | end | ||
| Line 173: | Line 124: | ||
end | end | ||
-- | -- 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()] | |||
if value and value ~= "" then | if value and value ~= "" then | ||
-- Apply transform | -- Apply transform | ||
if transformFunc then | if transformFunc then value = transformFunc(value) end | ||
-- | -- Handle metadata qualifiers | ||
local qualifiedProperty = propertyName | local qualifiedProperty = propertyName | ||
if next(metadata) then | if next(metadata) then | ||
| Line 194: | Line 143: | ||
table.insert(qualifiers, metaKey .. "=" .. metaValue) | table.insert(qualifiers, metaKey .. "=" .. metaValue) | ||
end | end | ||
table.sort(qualifiers) | table.sort(qualifiers) | ||
qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";") | qualifiedProperty = propertyName .. "#" .. table.concat(qualifiers, ";") | ||
end | end | ||
-- Set | -- Set property with array handling | ||
if properties[qualifiedProperty] then | if properties[qualifiedProperty] then | ||
if type(properties[qualifiedProperty]) ~= "table" then | if type(properties[qualifiedProperty]) ~= "table" then | ||
properties[qualifiedProperty] = {properties[qualifiedProperty]} | properties[qualifiedProperty] = {properties[qualifiedProperty]} | ||
end | end | ||
table.insert(properties[qualifiedProperty], value) | table.insert(properties[qualifiedProperty], value) | ||
else | else | ||
properties[qualifiedProperty] = value | properties[qualifiedProperty] = value | ||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
-- | -- Add simple property to parser function result | ||
local function addSimplePropertyToResult(result, propertyName, value, transformFunc, defaultValue) | local function addSimplePropertyToResult(result, propertyName, value, transformFunc, defaultValue) | ||
if value and value ~= "" and transformFunc then value = transformFunc(value) end | |||
if value and value ~= "" and transformFunc then | |||
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 231: | Line 171: | ||
return 1 | return 1 | ||
end | end | ||
return 0 | return 0 | ||
end | end | ||
-- | -- Enhanced fallback for mw.smw.set with complex mapping support. | ||
function p.generateEnhancedAnnotations(args, mappings, options) | function p.generateEnhancedAnnotations(args, mappings, options) | ||
args = args or {} | args = args or {} | ||
| Line 244: | Line 180: | ||
options = options or {} | options = options or {} | ||
-- | -- 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 251: | Line 187: | ||
local conditional = options.conditional or {} | local conditional = options.conditional or {} | ||
-- | -- Build annotation block | ||
local result = {} | local result = {} | ||
if not visible then table.insert(result, '<div style="display:none;">') end | |||
if not visible then | |||
table.insert(result, ' {{#set:') | table.insert(result, ' {{#set:') | ||
local propertyCount = 0 | local propertyCount = 0 | ||
-- | -- 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 | ||
if type(mapping) == "string" then | if type(mapping) == "string" then | ||
-- | -- Simple string mapping with case-insensitive lookup | ||
local value = args[mapping] or args[mapping:lower()] | |||
propertyCount = propertyCount + addSimplePropertyToResult(result, | propertyCount = propertyCount + addSimplePropertyToResult(result, | ||
fullPropertyName, | fullPropertyName, value, transform[property], default[property]) | ||
elseif type(mapping) == "table" then | elseif type(mapping) == "table" then | ||
if mapping.param 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, | propertyCount = propertyCount + addSimplePropertyToResult(result, | ||
fullPropertyName, | 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 284: | 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()] | |||
if value and value ~= "" then | if value and value ~= "" then | ||
-- Apply transform | -- Apply transform | ||
if transform[property] then | if transform[property] then value = transform[property](value) end | ||
-- Add metadata qualifiers | -- Add metadata qualifiers | ||
local qualifiedProperty = fullPropertyName | local qualifiedProperty = fullPropertyName | ||
if next(metadata) then | if next(metadata) then | ||
| Line 300: | Line 227: | ||
table.insert(qualifiers, metaKey .. "=" .. metaValue) | table.insert(qualifiers, metaKey .. "=" .. metaValue) | ||
end | end | ||
table.sort(qualifiers) | table.sort(qualifiers) | ||
qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";") | qualifiedProperty = fullPropertyName .. "#" .. table.concat(qualifiers, ";") | ||
end | end | ||
-- Add | -- 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 | ||
end | end | ||
end | end | ||
| Line 315: | Line 240: | ||
end | end | ||
-- Handle conditional properties | -- 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 324: | Line 249: | ||
end | end | ||
-- Close | -- Close parser function and wrapper | ||
table.insert(result, ' }}') | 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 | 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) | function p.setSemanticProperties(args, mappings, options) | ||
-- | -- 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 | ||
options = options or {} | options = options or {} | ||
| Line 365: | Line 276: | ||
local default = options.default or {} | local default = options.default or {} | ||
local prefix = options.prefix or "" | local prefix = options.prefix or "" | ||
local properties = {} | local properties = {} | ||
local semanticOutput = "" | |||
-- Process all | -- 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 | ||
if type(mapping) == "string" then | if type(mapping) == "string" then | ||
-- | -- Simple string mapping with case-insensitive lookup | ||
processSimpleMapping(properties, fullPropertyName, | -- 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, | 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 | ||
-- | -- Before setting properties, prune them | ||
local pruned, err = prune(properties) | |||
-- Set | if not pruned then | ||
local success, result = pcall(function() | -- 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 | ||
end | end | ||
result = result .. "\n }}\n</div>" | |||
return result | |||
end | end | ||
return p | return p | ||