/** * Semantic Web filters for OWL/RDF ontology generation * Provides Turtle/N3 formatting and RDF resource utilities */ import { kebabCase, pascalCase, camelCase, titleCase } from './case.js'; /** * Convert string to RDF resource URI * @param value - String to convert * @param baseUri - Optional base URI */ export function rdfResource(value: string, baseUri?: string): string { if (!value) return ''; // If already a full URI, return as-is if (value.match(/^https?:\/\//) || value.match(/^urn:/)) { return value; } // Clean and format the value const cleaned = value.replace(/[^a-zA-Z0-9\-_\/]/g, ''); if (baseUri) { return baseUri.endsWith('/') ? `${baseUri}${cleaned}` : `${baseUri}/${cleaned}`; } return cleaned; } /** * Convert string to RDF literal with optional language tag or datatype * @param value - String value * @param langOrType - Language tag (e.g., 'en') or datatype (e.g., 'xsd:dateTime') */ export function rdfLiteral(value: string, langOrType?: string): string { if (!value) return '""'; // Escape quotes and special characters const escaped = value.replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t'); if (!langOrType) { return `"${escaped}"`; } // Check if it's a datatype (contains colon) if (langOrType.includes(':') || langOrType.startsWith('xsd:')) { return `"${escaped}"^^${langOrType}`; } // It's a language tag return `"${escaped}"@${langOrType}`; } /** * Convert string to RDF datatype URI * @param type - Datatype name */ export function rdfDatatype(type: string): string { const xsdTypes: Record = { 'string': 'xsd:string', 'integer': 'xsd:integer', 'int': 'xsd:integer', 'float': 'xsd:float', 'double': 'xsd:double', 'decimal': 'xsd:decimal', 'boolean': 'xsd:boolean', 'bool': 'xsd:boolean', 'date': 'xsd:date', 'time': 'xsd:time', 'dateTime': 'xsd:dateTime', 'datetime': 'xsd:dateTime', 'duration': 'xsd:duration', 'uri': 'xsd:anyURI', 'url': 'xsd:anyURI', 'anyURI': 'xsd:anyURI', 'base64Binary': 'xsd:base64Binary', 'hexBinary': 'xsd:hexBinary', 'language': 'xsd:language', 'normalizedString': 'xsd:normalizedString', 'token': 'xsd:token', 'positiveInteger': 'xsd:positiveInteger', 'nonNegativeInteger': 'xsd:nonNegativeInteger', 'negativeInteger': 'xsd:negativeInteger', 'nonPositiveInteger': 'xsd:nonPositiveInteger', 'long': 'xsd:long', 'short': 'xsd:short', 'byte': 'xsd:byte', 'unsignedLong': 'xsd:unsignedLong', 'unsignedInt': 'xsd:unsignedInt', 'unsignedShort': 'xsd:unsignedShort', 'unsignedByte': 'xsd:unsignedByte' }; // Return mapped XSD type or original if already qualified return xsdTypes[type] || (type.includes(':') ? type : `xsd:${type}`); } /** * Convert string to RDF class reference with prefix * @param className - Class name * @param prefix - Optional namespace prefix */ export function rdfClass(className: string, prefix?: string): string { if (!className) return ''; const formattedClass = pascalCase(className); if (prefix) { const kebabPrefix = kebabCase(prefix); return `${kebabPrefix}:${formattedClass}`; } return formattedClass; } /** * Convert string to RDF property reference with prefix * @param propertyName - Property name * @param prefix - Optional namespace prefix */ export function rdfProperty(propertyName: string, prefix?: string): string { if (!propertyName) return ''; const formattedProperty = camelCase(propertyName); if (prefix) { const kebabPrefix = kebabCase(prefix); return `${kebabPrefix}:${formattedProperty}`; } return formattedProperty; } /** * Convert Date to ISO 8601 string for xsd:dateTime * @param date - Date object or string */ export function isoDate(date: Date | string): string { if (!date) return new Date().toISOString(); if (typeof date === 'string') { return new Date(date).toISOString(); } return date.toISOString(); } /** * Create SPARQL-safe identifier * @param value - String to make SPARQL-safe */ export function sparqlSafe(value: string): string { return value.replace(/[^a-zA-Z0-9_]/g, '_'); } /** * Generate RDF blank node identifier * @param id - Optional identifier */ export function blankNode(id?: string): string { if (id) { return `_:${sparqlSafe(id)}`; } // Generate random blank node ID const random = Math.random().toString(36).substr(2, 9); return `_:bn${random}`; } /** * Create OWL restriction expression * @param property - Property name * @param restrictionType - Type of restriction * @param value - Value for the restriction * @param prefix - Optional prefix */ export function owlRestriction( property: string, restrictionType: string, value: string, prefix?: string ): string { const propRef = rdfProperty(property, prefix); const restrictions: Record = { 'someValuesFrom': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:someValuesFrom ${value} ]`, 'allValuesFrom': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:allValuesFrom ${value} ]`, 'hasValue': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:hasValue ${value} ]`, 'cardinality': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:cardinality "${value}"^^xsd:nonNegativeInteger ]`, 'minCardinality': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:minCardinality "${value}"^^xsd:nonNegativeInteger ]`, 'maxCardinality': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:maxCardinality "${value}"^^xsd:nonNegativeInteger ]`, 'qualifiedCardinality': `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ; owl:qualifiedCardinality "${value}"^^xsd:nonNegativeInteger ]` }; return restrictions[restrictionType] || `[ rdf:type owl:Restriction ; owl:onProperty ${propRef} ]`; } /** * Create OWL class expression (union, intersection, complement) * @param classes - Array of class names * @param operator - 'union', 'intersection', or 'complement' * @param prefix - Optional prefix */ export function owlClassExpression( classes: string[], operator: 'union' | 'intersection' | 'complement', prefix?: string ): string { if (!classes || classes.length === 0) return ''; const classRefs = classes.map(cls => rdfClass(cls, prefix)); switch (operator) { case 'union': return `[ rdf:type owl:Class ; owl:unionOf ( ${classRefs.join(' ')} ) ]`; case 'intersection': return `[ rdf:type owl:Class ; owl:intersectionOf ( ${classRefs.join(' ')} ) ]`; case 'complement': return `[ rdf:type owl:Class ; owl:complementOf ${classRefs[0]} ]`; default: return classRefs.join(' , '); } } /** * Generate SKOS concept hierarchy * @param concept - Concept name * @param broader - Broader concept * @param narrower - Narrower concepts * @param prefix - Optional prefix */ export function skosConcept( concept: string, broader?: string, narrower?: string[], prefix?: string ): string { const conceptRef = rdfClass(concept, prefix); let result = `${conceptRef} rdf:type skos:Concept ;`; if (broader) { result += ` skos:broader ${rdfClass(broader, prefix)} ;`; } if (narrower && narrower.length > 0) { const narrowerRefs = narrower.map(n => rdfClass(n, prefix)); result += ` skos:narrower ${narrowerRefs.join(' , ')} ;`; } return result; } /** * Generate Dublin Core metadata * @param title - Title * @param creator - Creator * @param created - Creation date * @param description - Description */ export function dublinCore( title?: string, creator?: string, created?: string | Date, description?: string ): string { const metadata: string[] = []; if (title) { metadata.push(`dcterms:title ${rdfLiteral(title, 'en')}`); } if (creator) { metadata.push(`dcterms:creator ${rdfLiteral(creator, 'en')}`); } if (created) { const createdDate = typeof created === 'string' ? created : isoDate(created); metadata.push(`dcterms:created ${rdfLiteral(createdDate, 'xsd:dateTime')}`); } if (description) { metadata.push(`dcterms:description ${rdfLiteral(description, 'en')}`); } return metadata.join(' ; '); } /** * Validate OWL ontology structure * @param ontology - Ontology string content */ export function validateOwl(ontology: string): { valid: boolean; errors: string[] } { const errors: string[] = []; // Basic syntax checks if (!ontology.includes('rdf:type owl:Ontology')) { errors.push('Missing ontology declaration'); } if (!ontology.includes('@prefix owl:')) { errors.push('Missing OWL namespace declaration'); } if (!ontology.includes('@prefix rdfs:')) { errors.push('Missing RDFS namespace declaration'); } // Check for proper class declarations const classPattern = /\w+:\w+\s+rdf:type\s+owl:Class/g; const classMatches = ontology.match(classPattern); if (!classMatches || classMatches.length === 0) { errors.push('No OWL classes found'); } // Check for unmatched brackets const openBrackets = (ontology.match(/\[/g) || []).length; const closeBrackets = (ontology.match(/\]/g) || []).length; if (openBrackets !== closeBrackets) { errors.push('Unmatched brackets in class expressions'); } // Check for proper statement termination const lines = ontology.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line && !line.startsWith('#') && !line.startsWith('@') && !line.endsWith('.') && !line.endsWith(';') && !line.endsWith(',') && !line.endsWith('[') && !line.endsWith(']') && !line.endsWith('(') && !line.endsWith(')')) { errors.push(`Line ${i + 1}: Statement may be missing terminator`); } } return { valid: errors.length === 0, errors }; } // Export all semantic filters export const semanticFilters = { rdfResource, rdfLiteral, rdfDatatype, rdfClass, rdfProperty, isoDate, sparqlSafe, blankNode, owlRestriction, owlClassExpression, skosConcept, dublinCore, validateOwl };