Jump to content

Module:SemanticCategoryHelpers

From ICANNWiki

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

-- Module:SemanticCategoryHelpers
-- Provides utilities for semantic property and category handling in templates.
-- Extracted from TemplateHelpers to improve modularity and focus.
--
-- This module combines semantic property and category utilities that are
-- frequently used together in templates. It provides functions for:
-- * Splitting multi-value strings (e.g., "value1; value2 and value3")
-- * Building category tags from category names
-- * Adding categories based on canonical mappings
-- * Processing multi-value semantic properties with a unified approach
-- * Generating semantic properties based on configuration

local p = {}

-- Dependencies
local CanonicalForms = require('Module:CanonicalForms')
local SemanticAnnotations = require('Module:SemanticAnnotations')

--------------------------------------------------------------------------------
-- Property Type Registry
--------------------------------------------------------------------------------

-- Registry of property types with their configurations
-- Each property type has:
-- - getPropertyName: Function that returns the property name from ConfigRepository
-- - processor: Function that processes a value for this property type
local propertyTypes = {
    country = {
        getPropertyName = function() 
            return require('Module:ConfigRepository').semanticProperties.country
        end,
        processor = function(value)
            local CountryData = require('Module:CountryData')
            local normalized = CountryData.normalizeCountryName(value)
            if normalized == "(Unrecognized)" then
                return nil
            end
            return normalized
        end
    },
    region = {
        getPropertyName = function() 
            return require('Module:ConfigRepository').semanticProperties.region
        end,
        processor = function(value)
            if value == "(Unrecognized)" then
                return nil
            end
            return value:match("^%s*(.-)%s*$") -- Trim whitespace
        end
    },
    language = {
        getPropertyName = function() 
            return require('Module:ConfigRepository').semanticProperties.language
        end,
        processor = function(value)
            return require('Module:NormalizationLanguage').normalize(value)
        end
    },
    person = {
        getPropertyName = function() 
            return require('Module:ConfigRepository').semanticProperties.person
        end
    }
}

--------------------------------------------------------------------------------
-- Core Utilities
--------------------------------------------------------------------------------

-- Default delimiters for splitMultiValueString
-- Defined once as an upvalue to avoid recreating on each function call
local defaultDelimiters = {
    {pattern = "%s+and%s+", replacement = ";"},
    {pattern = ";%s*", replacement = ";"}
}

-- Semicolon-only pattern for backward compatibility with splitSemicolonValues
-- Exposed as a module-level constant for use by other modules
p.SEMICOLON_PATTERN = {{pattern = ";%s*", replacement = ";"}}

-- Generic function to split multi-value strings with various delimiters
-- Returns an array of individual values
function p.splitMultiValueString(value, delimiters)
    if not value or value == "" then return {} end
    
    -- Use provided delimiters or default ones
    delimiters = delimiters or defaultDelimiters
    
    -- Standardize all delimiters to semicolons
    local standardizedInput = value
    for _, delimiter in ipairs(delimiters) do
        standardizedInput = standardizedInput:gsub(delimiter.pattern, delimiter.replacement)
    end
    
    -- Pre-allocate table based on delimiter count
    -- Count semicolons to estimate the number of items
    local count = 0
    for _ in standardizedInput:gmatch(";") do 
        count = count + 1 
    end
    
    -- Pre-allocate table with estimated size (count+1 for the last item)
    local items = {}
    
    -- Split by semicolons and return the array
    local index = 1
    for item in standardizedInput:gmatch("[^;]+") do
        local trimmed = item:match("^%s*(.-)%s*$")
        if trimmed and trimmed ~= "" then
            items[index] = trimmed
            index = index + 1
        end
    end
    
    return items
end

-- Helper function to check if a field contains multiple values
function p.isMultiValueField(value)
    if not value or value == "" then return false end
    
    -- Check for common multi-value delimiters
    return value:match(";") or value:match("%s+and%s+")
end

--------------------------------------------------------------------------------
-- Category Utilities
--------------------------------------------------------------------------------

-- Ensures a category string is properly wrapped in MediaWiki syntax
function p.formatCategoryName(categoryName)
    if not categoryName or categoryName == "" then return "" end
    
    -- Already has full MediaWiki syntax
    if categoryName:match("^%[%[Category:[^%]]+%]%]$") then
        return categoryName
    end
    
    -- Has partial syntax, normalize it
    if categoryName:match("^Category:") then
        return string.format("[[%s]]", categoryName)
    end
    
    -- Plain category name, add full syntax
    return string.format("[[Category:%s]]", categoryName)
end

-- Builds a category string from a table of category names
-- Pre-allocates the formatted table for better performance
function p.buildCategories(categories)
    if not categories or #categories == 0 then return "" end
    
    -- Pre-allocate formatted table based on input size
    local formatted = {}
    local index = 1
    
    for _, cat in ipairs(categories) do
        -- Use the formatCategoryName function to ensure proper syntax
        formatted[index] = p.formatCategoryName(cat)
        index = index + 1
    end
    return table.concat(formatted, "\n")
end

-- Adds categories based on a canonical mapping
function p.addMappingCategories(value, mapping)
    if not value or value == "" or not mapping then return {} end
    local categories = {}
    local canonical = select(1, CanonicalForms.normalize(value, mapping))
    
    if canonical then
        for _, group in ipairs(mapping) do
            if group.canonical == canonical and group.category then
                table.insert(categories, group.category)
                break
            end
        end
    end
    
    return categories
end

-- Generic function to add multi-value categories
-- This is a generalized helper that can be used for any multi-value category field
function p.addMultiValueCategories(value, processor, categories, options)
    if not value or value == "" then return categories end
    
    options = options or {}
    
    -- Get the values to process
    local items
    if options.valueGetter and type(options.valueGetter) == "function" then
        -- Use custom value getter if provided
        items = options.valueGetter(value)
    else
        -- Default to splitting the string
        items = p.splitMultiValueString(value)
    end
    
    -- Pre-allocate space in the categories table
    -- Estimate the number of new categories to add
    local currentSize = #categories
    local estimatedNewSize = currentSize + #items
    
    -- Process each item and add as a category
    for _, item in ipairs(items) do
        -- Apply processor if provided
        local processedItem = item
        if processor and type(processor) == "function" then
            processedItem = processor(item)
        end
        
        -- Only add if valid
        if processedItem and processedItem ~= "" then
            categories[currentSize + 1] = processedItem
            currentSize = currentSize + 1
        end
    end
    
    return categories
end

-- Splits a region string that may contain "and" conjunctions
-- Returns an array of individual region names
-- This is now a wrapper around splitMultiValueString for backward compatibility
function p.splitRegionCategories(regionValue)
    return p.splitMultiValueString(regionValue)
end

--------------------------------------------------------------------------------
-- Semantic Property Helpers
--------------------------------------------------------------------------------

-- Unified function to add semantic properties for any property type
-- @param propertyType - The type of property (e.g., "country", "region", "language")
-- @param value - The value to process
-- @param semanticOutput - The current semantic output to append to
-- @param options - Additional options for processing
-- @return The updated semantic output
function p.addSemanticProperties(propertyType, value, semanticOutput, options)
    if not value or value == "" then return semanticOutput end
    
    options = options or {}
    local processedItems = {}
    
    -- Get configuration for this property type
    local config = propertyTypes[propertyType]
    if not config then
        -- Check if propertyType is a key in ConfigRepository.semanticProperties
        local ConfigRepository = require('Module:ConfigRepository')
        local propertyName = ConfigRepository.semanticProperties[propertyType]
        
        if propertyName then
            -- Create a dynamic config for this property
            config = {
                getPropertyName = function() return propertyName end,
                processor = options.processor
            }
        else
            -- If it's a direct property name, use it as is
            config = {
                getPropertyName = function() return propertyType end,
                processor = options.processor
            }
        end
    end
    
    -- Get property name from config
    local propertyName = config.getPropertyName()
    
    -- Get the values to process
    local items
    if options.valueGetter and type(options.valueGetter) == "function" then
        -- Use custom value getter if provided
        items = options.valueGetter(value)
    else
        -- Default to splitting the string
        items = p.splitMultiValueString(value)
    end
    
    -- For non-SMW case, collect property HTML fragments in a table for efficient concatenation
    local propertyHtml = {}
    
    -- Process each item and add as a semantic property
    for _, item in ipairs(items) do
        -- Apply processor if provided
        local processedItem = item
        if config.processor and type(config.processor) == "function" then
            processedItem = config.processor(item)
        end
        
        -- Only add if valid and not already processed
        if processedItem and processedItem ~= "" and not processedItems[processedItem] then
            processedItems[processedItem] = true
            
            -- Add as semantic property
            if mw.smw then
                mw.smw.set({[propertyName] = processedItem})
            else
                -- Collect HTML fragments instead of concatenating strings
                table.insert(propertyHtml, '<div style="display:none;">')
                table.insert(propertyHtml, '  {{#set: ' .. propertyName .. '=' .. processedItem .. ' }}')
                table.insert(propertyHtml, '</div>')
            end
        end
    end
    
    -- For non-SMW case, concatenate all property HTML fragments at once
    if not mw.smw and #propertyHtml > 0 then
        semanticOutput = semanticOutput .. "\n" .. table.concat(propertyHtml, "\n")
    end
    
    return semanticOutput
end

-- Helper function to process additional properties with multi-value support
-- This standardizes how additional properties are handled across templates
function p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
    if not semanticConfig or not semanticConfig.additionalProperties then
        return semanticOutput
    end
    
    skipProperties = skipProperties or {}
    
    for property, sourceFields in pairs(semanticConfig.additionalProperties) do
        -- Skip properties that are handled separately
        if not skipProperties[property] then
            for _, fieldName in ipairs(sourceFields) do
                if args[fieldName] and args[fieldName] ~= "" then
                    local value = args[fieldName]
                    
                    -- Find the property type key in ConfigRepository that matches this property
                    local propertyTypeKey = nil
                    local ConfigRepository = require('Module:ConfigRepository')
                    for key, name in pairs(ConfigRepository.semanticProperties) do
                        if name == property then
                            propertyTypeKey = key
                            break
                        end
                    end
                    
                    -- If no matching key found, use the property name directly
                    if not propertyTypeKey then
                        propertyTypeKey = property
                    end
                    
                    -- Create processor option if transformation is available
                    local options = {}
                    if semanticConfig.transforms and semanticConfig.transforms[property] then
                        options.processor = semanticConfig.transforms[property]
                    end
                    
                    -- Use the unified function for all properties
                    semanticOutput = p.addSemanticProperties(
                        propertyTypeKey,
                        value,
                        semanticOutput,
                        options
                    )
                end
            end
        end
    end
    
    return semanticOutput
end

-- Generates semantic properties based on configuration
-- @param args - Template parameters
-- @param semanticConfig - Config with properties, transforms, additionalProperties
-- @param options - Options: transform (functions), skipProperties (to exclude)
-- @return Wikitext with semantic annotations
function p.generateSemanticProperties(args, semanticConfig, options)
    if not args or not semanticConfig then return "" end
    
    local SemanticAnnotations = require('Module:SemanticAnnotations')
    options = options or {}
    
    -- Set options
    local semanticOptions = {
        transform = semanticConfig.transforms or options.transform
    }
    
    -- Set basic properties
    local semanticOutput = SemanticAnnotations.setSemanticProperties(
        args, 
        semanticConfig.properties, 
        semanticOptions
    )
    
    -- Process additional properties with multi-value support
    local skipProperties = options.skipProperties or {}
    semanticOutput = p.processAdditionalProperties(args, semanticConfig, semanticOutput, skipProperties)
    
    return semanticOutput
end

return p