Module:SemanticAnnotations: Difference between revisions
// via Wikitext Extension for VSCode |
// via Wikitext Extension for VSCode |
||
| (35 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 | 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 | ||
end | 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 54: | 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 | ||
if type(param) == "string" then | if type(param) == "string" then | ||
local fullPropertyName = prefix .. property | local fullPropertyName = prefix .. property | ||
local value = args | local _, value = TemplateHelpers.getFieldValue(args, { key = param }) | ||
-- Apply transform if | -- Apply transform if needed | ||
if value and transform[property] then | if value and transform[property] then value = transform[property](value) end | ||
-- | -- 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 91: | Line 64: | ||
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 100: | 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 191: | 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 213: | 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 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. | ||
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 {} | ||
-- | -- 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 {} | ||
-- | -- 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 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()] | |||
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 319: | 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 | ||
| Line 333: | 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 342: | 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 384: | 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 = "" | 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.is_subobject then | if mapping.is_subobject then | ||
-- | -- Subobject definition | ||
local subobjectProperties = mapping.properties or {} | local subobjectProperties = mapping.properties or {} | ||
local actualProperties = {} | local actualProperties = {} | ||
-- Process | -- Process subobject properties | ||
for subPropName, subPropValue in pairs(subobjectProperties) do | for subPropName, subPropValue in pairs(subobjectProperties) do | ||
if type(subPropValue) == "table" and subPropValue.param then | if type(subPropValue) == "table" and subPropValue.param then | ||
-- Object with param reference | -- Object with param reference | ||
local paramName = subPropValue.param | local paramName = subPropValue.param | ||
if args[paramName] and args[paramName] ~= "" then | if args[paramName] and args[paramName] ~= "" then | ||
local value = args[paramName] | local value = args[paramName] | ||
if subPropValue.transform and type(subPropValue.transform) == "function" then | if subPropValue.transform and type(subPropValue.transform) == "function" then | ||
value = subPropValue.transform(value) | value = subPropValue.transform(value) | ||
end | end | ||
actualProperties[subPropName] = value | actualProperties[subPropName] = value | ||
end | end | ||
elseif type(subPropValue) == "string" then | elseif type(subPropValue) == "string" then | ||
-- | -- String mapping or static value | ||
if args[subPropValue] and args[subPropValue] ~= "" then | if args[subPropValue] and args[subPropValue] ~= "" then | ||
actualProperties[subPropName] = args[subPropValue] | actualProperties[subPropName] = args[subPropValue] | ||
else | else | ||
actualProperties[subPropName] = subPropValue | actualProperties[subPropName] = subPropValue | ||
end | end | ||
| Line 437: | Line 316: | ||
end | end | ||
-- | -- Create subobject if properties exist | ||
if next(actualProperties) then | if next(actualProperties) then | ||
-- Generate | -- Generate ID | ||
local idPrefix = mapping.id_prefix or "subobj" | local idPrefix = mapping.id_prefix or "subobj" | ||
local idValue = "" | local idValue = "" | ||
local primaryProp = mapping.primary_property | |||
if primaryProp and actualProperties[primaryProp] then | if primaryProp and actualProperties[primaryProp] then | ||
idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_") | idValue = tostring(actualProperties[primaryProp]):gsub("[^%w]", "_") | ||
else | else | ||
idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999) | idValue = tostring(os.time() % 10000) .. "_" .. math.random(1000, 9999) | ||
end | end | ||
local subobjectId = idPrefix .. "_" .. idValue | local subobjectId = idPrefix .. "_" .. idValue | ||
-- Create | -- 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 | ||
end | end | ||
elseif mapping.param then | elseif mapping.param then | ||
-- Single mapping with object structure | -- Single mapping with object structure with case-insensitive lookup | ||
processSimpleMapping(properties, fullPropertyName, | 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 success then | if next(pruned) then | ||
local success, result = pcall(function() return mw.smw.set(pruned) end) | |||
else | if success then return semanticOutput | ||
else return p.generateEnhancedAnnotations(args, mappings, options) .. semanticOutput | |||
end | end | ||
end | end | ||
| Line 504: | Line 378: | ||
end | end | ||
-- | -- Generate #subobject parser function with optional ID | ||
function p.generateSmwSubobjectFragment(properties, id) | function p.generateSmwSubobjectFragment(properties, id) | ||
local result = '<div style="display:none;">\n {{#subobject:' | local result = '<div style="display:none;">\n {{#subobject:' | ||
if id and id ~= "" then result = result .. "|@" .. id end | |||
if id and id ~= "" then | |||
for propName, propValue in pairs(properties) do | for propName, propValue in pairs(properties) do | ||
| Line 522: | Line 392: | ||
return result | return result | ||
end | end | ||
return p | return p | ||