UNPKG

xsd-lookup

Version:
1,014 lines 114 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Schema = void 0; const fs = __importStar(require("fs")); const xmldom_1 = require("@xmldom/xmldom"); class Schema { constructor(xsdFilePath, includeFiles = []) { this.maxCacheSize = 10000; // Initialize caches and metrics first this.initializeCaches(); this.doc = this.loadXml(xsdFilePath); // Merge include files if any for (const includeFile of includeFiles) { const includeDoc = this.loadXml(includeFile); this.mergeXsds(this.doc, includeDoc); } // Build indexes this.schemaIndex = this.indexSchema(this.doc.documentElement); this.elementMap = this.buildElementMap(); this.elementContexts = this.schemaIndex.elementContexts; } /** * Initialize all cache structures with empty maps */ initializeCaches() { this.cache = { hierarchyLookups: new Map(), definitionReachability: new Map(), elementSearchResults: new Map(), attributeCache: new Map(), hierarchyValidation: new Map(), elementDefinitionCache: new Map() }; } /** * Clear all caches by reinitializing them */ clearCache() { this.initializeCaches(); } /** * Ensure cache sizes don't exceed the maximum limit using simple LRU eviction */ ensureCacheSize() { if (this.cache.hierarchyLookups.size > this.maxCacheSize) { // Simple LRU: clear oldest half const entries = Array.from(this.cache.hierarchyLookups.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.hierarchyLookups.clear(); toKeep.forEach(([key, value]) => this.cache.hierarchyLookups.set(key, value)); } // Apply same logic to other caches if (this.cache.definitionReachability.size > this.maxCacheSize) { const entries = Array.from(this.cache.definitionReachability.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.definitionReachability.clear(); toKeep.forEach(([key, value]) => this.cache.definitionReachability.set(key, value)); } if (this.cache.elementSearchResults.size > this.maxCacheSize) { const entries = Array.from(this.cache.elementSearchResults.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.elementSearchResults.clear(); toKeep.forEach(([key, value]) => this.cache.elementSearchResults.set(key, value)); } if (this.cache.attributeCache.size > this.maxCacheSize) { const entries = Array.from(this.cache.attributeCache.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.attributeCache.clear(); toKeep.forEach(([key, value]) => this.cache.attributeCache.set(key, value)); } if (this.cache.elementDefinitionCache.size > this.maxCacheSize) { const entries = Array.from(this.cache.elementDefinitionCache.entries()); const toKeep = entries.slice(-Math.floor(this.maxCacheSize / 2)); this.cache.elementDefinitionCache.clear(); toKeep.forEach(([key, value]) => this.cache.elementDefinitionCache.set(key, value)); } } /** * Load and parse an XML file into a DOM Document * @param filePath The path to the XML file to load * @returns Parsed DOM Document */ loadXml(filePath) { const xml = fs.readFileSync(filePath, 'utf8'); return new xmldom_1.DOMParser().parseFromString(xml, 'application/xml'); } /** * Merge included XSD documents into the main schema document * @param mainDoc The main schema document to merge into * @param includeDoc The included schema document to merge from */ mergeXsds(mainDoc, includeDoc) { const mainSchema = mainDoc.documentElement; const includeSchema = includeDoc.documentElement; for (let i = 0; i < includeSchema.childNodes.length; i++) { const node = includeSchema.childNodes[i]; if (node.nodeType === 1) { mainSchema.appendChild(node.cloneNode(true)); } } } /** * Recursively collect all element definitions from the schema DOM * @param node The current node to examine * @param parentName The name of the parent element * @param elements Array to collect found elements into * @param ns The XML Schema namespace prefix * @returns Array of collected elements with their parent information */ collectElements(node, parentName, elements = [], ns = 'xs:') { if (!node) return elements; if (node.nodeType === 1) { const element = node; if (element.nodeName === ns + 'element' && element.getAttribute('name')) { elements.push({ name: element.getAttribute('name'), parent: parentName, node: element }); } // Recurse into children for (let i = 0; i < element.childNodes.length; i++) { this.collectElements(element.childNodes[i], element.getAttribute('name') || parentName, elements, ns); } } return elements; } /** * Build a map of element names to their definitions and parent relationships * @returns Record mapping element names to arrays of their definitions */ buildElementMap() { const elements = this.collectElements(this.doc.documentElement, null); const elementMap = {}; elements.forEach(e => { if (!elementMap[e.name]) elementMap[e.name] = []; elementMap[e.name].push({ parent: e.parent, node: e.node }); }); return elementMap; } /** * Index the schema by collecting all global elements, groups, attribute groups, and types * @param root The root schema element * @param ns The XML Schema namespace prefix * @returns Complete schema index with all definitions and contexts */ indexSchema(root, ns = 'xs:') { const elements = {}; // Changed to arrays const groups = {}; const attributeGroups = {}; const types = {}; // First, collect only direct children of the schema root (truly global elements) for (let i = 0; i < root.childNodes.length; i++) { const child = root.childNodes[i]; if (child.nodeType === 1) { const element = child; if (element.nodeName === ns + 'element' && element.getAttribute('name')) { const name = element.getAttribute('name'); if (!elements[name]) elements[name] = []; elements[name].push(element); } else if (element.nodeName === ns + 'group' && element.getAttribute('name')) { groups[element.getAttribute('name')] = element; } else if (element.nodeName === ns + 'attributeGroup' && element.getAttribute('name')) { attributeGroups[element.getAttribute('name')] = element; } else if (element.nodeName === ns + 'complexType' && element.getAttribute('name')) { types[element.getAttribute('name')] = element; } else if (element.nodeName === ns + 'simpleType' && element.getAttribute('name')) { types[element.getAttribute('name')] = element; } } } // Then walk recursively to collect all types, groups, and attribute groups (which can be nested) const walkForTypesAndGroups = (node) => { if (!node || node.nodeType !== 1) return; const element = node; // Only collect types and groups, not nested elements if (element.nodeName === ns + 'group' && element.getAttribute('name')) { groups[element.getAttribute('name')] = element; } if (element.nodeName === ns + 'attributeGroup' && element.getAttribute('name')) { attributeGroups[element.getAttribute('name')] = element; } if (element.nodeName === ns + 'complexType' && element.getAttribute('name')) { types[element.getAttribute('name')] = element; } if (element.nodeName === ns + 'simpleType' && element.getAttribute('name')) { types[element.getAttribute('name')] = element; } // Recurse into children for (let i = 0; i < element.childNodes.length; i++) { walkForTypesAndGroups(element.childNodes[i]); } }; walkForTypesAndGroups(root); // Build comprehensive element contexts including group membership const elementContexts = this.buildElementContexts(elements, groups, types); return { elements, groups, attributeGroups, types, elementContexts }; } /** * Build comprehensive element contexts, including elements reachable through groups */ buildElementContexts(globalElements, groups, types) { const elementContexts = {}; const ns = 'xs:'; // Build type-to-element mapping const typeToElements = this.buildTypeToElementMapping(globalElements, types); // First, add all global elements as their own contexts for (const [elementName, elements] of Object.entries(globalElements)) { if (!elementContexts[elementName]) elementContexts[elementName] = []; for (const element of elements) { elementContexts[elementName].push({ element, groups: [], // Global elements don't belong to groups directly parents: [] // Will be filled in later when we analyze where they can appear }); } } // Then, traverse all groups to find elements defined within them for (const [groupName, groupElement] of Object.entries(groups)) { this.extractElementsFromGroup(groupElement, groupName, elementContexts, groups, types, ns); } // IMPORTANT: Also traverse all global elements to find inline element definitions // This captures cases like param under params, where param is defined inline // AND handles type references in context (e.g., library element using interrupt_library type) for (const [elementName, elements] of Object.entries(globalElements)) { for (const element of elements) { this.extractInlineElementsFromElement(element, elementName, elementContexts, groups, types, ns, [elementName]); } } return elementContexts; } /** * Extract all elements from a group and add them to element contexts */ extractElementsFromGroup(groupElement, groupName, elementContexts, groups, types, ns, visitedGroups = new Set()) { // Prevent infinite recursion in group references if (visitedGroups.has(groupName)) return; visitedGroups.add(groupName); const extractElements = (node, currentGroups) => { if (!node || node.nodeType !== 1) return; // If this is an element definition, add it to contexts if (node.nodeName === ns + 'element' && node.getAttribute('name')) { const elementName = node.getAttribute('name'); if (!elementContexts[elementName]) elementContexts[elementName] = []; // Check if we already have this exact element in this group context const existingContext = elementContexts[elementName].find(ctx => ctx.element === node && JSON.stringify(ctx.groups.sort()) === JSON.stringify(currentGroups.sort())); if (!existingContext) { elementContexts[elementName].push({ element: node, groups: [...currentGroups], parents: [] // Will be filled in later }); } } // If this is a group reference, recursively extract from the referenced group if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref'); const refGroup = groups[refGroupName]; if (refGroup && !visitedGroups.has(refGroupName)) { this.extractElementsFromGroup(refGroup, refGroupName, elementContexts, groups, types, ns, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = types[baseName]; if (baseType) { // Extract elements from the base type within the current element's context // For group elements, we need to find the parent element let parentElement = node.parentNode; while (parentElement && parentElement.nodeType === 1) { const parentElem = parentElement; if (parentElem.nodeName === ns + 'element') { const parentElementName = parentElem.getAttribute('name'); if (parentElementName) { // Extract elements from the base type with the parent element as context this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, ns, new Set(), [parentElementName]); } break; } parentElement = parentElement.parentNode; } } } // Recursively process all children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { extractElements(child, currentGroups); } } }; // Start extraction with the current group in the context extractElements(groupElement, [groupName]); } /** * Extract elements from a group while maintaining parent context information */ extractElementsFromGroupWithParentContext(groupElement, groupName, elementContexts, groups, types, ns, parentContext, visitedGroups = new Set()) { // Prevent infinite recursion if (visitedGroups.has(groupName)) return; visitedGroups.add(groupName); const extractElements = (node) => { if (!node || node.nodeType !== 1) return; // If this is an element definition, add it to contexts with group and parent info if (node.nodeName === ns + 'element' && node.getAttribute('name')) { const elementName = node.getAttribute('name'); if (!elementContexts[elementName]) elementContexts[elementName] = []; // Add context with both group membership and parent information elementContexts[elementName].push({ element: node, groups: [groupName], parents: [...parentContext] // Use the passed parent context }); } // If this is a group reference, recursively extract if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref'); const refGroup = groups[refGroupName]; if (refGroup && !visitedGroups.has(refGroupName)) { this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, ns, parentContext, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = types[baseName]; if (baseType) { // Extract elements from the base type within the current element's context // For group elements with parent context, we need to find the parent element let parentElement = node.parentNode; while (parentElement && parentElement.nodeType === 1) { const parentElem = parentElement; if (parentElem.nodeName === ns + 'element') { const parentElementName = parentElem.getAttribute('name'); if (parentElementName) { // Extract elements from the base type with the parent element as context this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, ns, new Set(), [parentElementName, ...parentContext]); } break; } parentElement = parentElement.parentNode; } } } // Recursively process all children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { extractElements(child); } } }; extractElements(groupElement); } /** * Extract all elements from a complex type and add them to element contexts */ extractElementsFromType(typeElement, typeName, elementContexts, groups, types, ns, visitedTypes = new Set(), parentElementNames = []) { // Prevent infinite recursion in type references if (visitedTypes.has(typeName)) return; visitedTypes.add(typeName); // Use the provided parent element names instead of the type name const currentParents = parentElementNames.length > 0 ? parentElementNames : [typeName]; const extractElements = (node, currentParents) => { if (!node || node.nodeType !== 1) return; // If this is an element definition, add it to contexts if (node.nodeName === ns + 'element' && node.getAttribute('name')) { const elementName = node.getAttribute('name'); if (!elementContexts[elementName]) elementContexts[elementName] = []; // Check if we already have this exact element in this parent context const existingContext = elementContexts[elementName].find(ctx => ctx.element === node && JSON.stringify(ctx.parents.sort()) === JSON.stringify(currentParents.sort())); if (!existingContext) { elementContexts[elementName].push({ element: node, groups: [], // Will be filled if this element is found through group references parents: [...currentParents] }); // Check if this element also has a type reference - if so, extract elements from that type const typeAttr = node.getAttribute('type'); if (typeAttr && types[typeAttr] && !visitedTypes.has(typeAttr)) { // Extract elements from the referenced type with this element as parent this.extractElementsFromType(types[typeAttr], typeAttr, elementContexts, groups, types, ns, new Set(), [elementName, ...currentParents]); } } } // If this is a group reference, extract elements from the group and mark with group membership if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref'); const refGroup = groups[refGroupName]; if (refGroup) { // Extract elements from the group and mark them with group membership // Only pass the immediate parent, not the full chain const immediateParent = currentParents.length > 0 ? [currentParents[0]] : []; this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, ns, immediateParent); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = types[baseName]; if (baseType && !visitedTypes.has(baseName)) { // Extract elements from the base type with the same parent context this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, ns, new Set([...visitedTypes, baseName]), currentParents); } } // Recursively process all children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { extractElements(child, currentParents); } } }; // Start extraction with the current parent element names extractElements(typeElement, currentParents); } /** * Extract inline elements from a global element definition * This captures elements like param under params that are defined inline */ extractInlineElementsFromElement(parentElement, parentElementName, elementContexts, groups, types, ns, initialParents = []) { const extractInlineElements = (node, currentParents, isRootElement = false) => { if (!node || node.nodeType !== 1) return; // If this is an inline element definition, add it to contexts if (node.nodeName === ns + 'element' && node.getAttribute('name')) { const elementName = node.getAttribute('name'); if (!elementContexts[elementName]) elementContexts[elementName] = []; // Only add to contexts if this is not the root element we started with if (!isRootElement) { // Add this inline element with its parent context elementContexts[elementName].push({ element: node, groups: [], // Inline elements don't belong to groups directly parents: [...currentParents] }); } // IMPORTANT: When we find an element, it becomes a potential parent for nested elements // Continue recursion with this element added to the parent chain // BUT: Don't add the root element to its own parent chain const newParents = isRootElement ? currentParents : [elementName, ...currentParents]; // Check if this element has a type reference - if so, extract elements from that type const typeAttr = node.getAttribute('type'); if (typeAttr && types[typeAttr]) { // Extract elements from the referenced type with this element as parent this.extractElementsFromType(types[typeAttr], typeAttr, elementContexts, groups, types, ns, new Set(), [elementName]); } for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { extractInlineElements(child, newParents, false); } } return; // Don't process children again with the old parent chain } // If this is a group reference, extract elements from the group if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref'); const refGroup = groups[refGroupName]; if (refGroup) { this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, ns, currentParents); } } // Handle type extensions - extract elements from the base type if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = types[baseName]; if (baseType) { // Extract elements from the base type with the same parent context this.extractElementsFromType(baseType, baseName, elementContexts, groups, types, ns, new Set(), currentParents); } } // Recursively process all children with the same parent context for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { extractInlineElements(child, currentParents, false); } } }; // Start extraction with the initial parents as context, marking the root element extractInlineElements(parentElement, initialParents, true); } getElementDefinition(elementName, hierarchy = []) { // Create cache key from element name and hierarchy const hierarchyKey = hierarchy.length > 0 ? hierarchy.join('|') : ''; const fullCacheKey = `${elementName}::${hierarchyKey}`; // Check if we have an exact match in cache if (this.cache.elementDefinitionCache.has(fullCacheKey)) { return this.cache.elementDefinitionCache.get(fullCacheKey); } // Check for partial matches in cache - look for any cached key that starts with our element name // and has a hierarchy that our current hierarchy extends if (hierarchy.length > 0) { for (const [cachedKey, cachedElement] of this.cache.elementDefinitionCache) { if (cachedKey.startsWith(`${elementName}::`)) { // Extract the cached hierarchy const cachedHierarchyStr = cachedKey.substring(`${elementName}::`.length); if (cachedHierarchyStr === '') continue; // Skip global element cache entries const cachedHierarchy = cachedHierarchyStr.split('|'); // Check if the current hierarchy starts with the cached hierarchy // If cached: [parent, grandparent] and current: [parent, grandparent, great-grandparent] // then we can use the cached result since it's a more specific match if (cachedHierarchy.length <= hierarchy.length) { let isMatch = true; for (let i = 0; i < cachedHierarchy.length; i++) { if (cachedHierarchy[i] !== hierarchy[i]) { isMatch = false; break; } } if (isMatch && cachedElement) { // Found a matching cached result for a shorter hierarchy // Cache this result for the current full hierarchy too this.cache.elementDefinitionCache.set(fullCacheKey, cachedElement); this.ensureCacheSize(); return cachedElement; } } } } } // No cache hit, perform the actual search let result; // Step 1: If no hierarchy provided, only return global elements if (hierarchy.length === 0) { // Look for global elements (direct children of schema root) const globalElements = this.getGlobalElementDefinitions(elementName); result = globalElements.length > 0 ? globalElements[0] : undefined; } else { // Step 2: INCREMENTAL HIERARCHY APPROACH // hierarchy = [immediate_parent, grandparent, great_grandparent, ...] // Try each level incrementally: bottom-up expansion, then top-down search for (let level = 1; level <= hierarchy.length; level++) { // Take the first 'level' elements from hierarchy (bottom-up expansion) const currentHierarchy = hierarchy.slice(0, level); // Reverse for top-down search: [great_grandparent, ..., grandparent, immediate_parent] const topDownHierarchy = [...currentHierarchy].reverse(); // Try to find element with this hierarchy level using top-down search const foundElement = this.findElementTopDown(elementName, topDownHierarchy); if (foundElement) { // Found a definition at this level result = foundElement; // Cache this intermediate result too (it might be useful for future lookups) const intermediateCacheKey = `${elementName}::${currentHierarchy.join('|')}`; this.cache.elementDefinitionCache.set(intermediateCacheKey, result); break; // Exit the loop since we found a match } } } // Cache the final result (even if undefined) this.cache.elementDefinitionCache.set(fullCacheKey, result); this.ensureCacheSize(); return result; } /** * Get global element definitions by name (direct children of schema root only) * @param elementName The name of the element to find * @returns Array of global element definitions */ getGlobalElementDefinitions(elementName) { // Only return truly global elements (direct children of schema root) if (this.schemaIndex.elements[elementName]) { return this.schemaIndex.elements[elementName]; } // Do NOT fall back to elementMap as it contains nested elements too return []; } /** * Get global element or type definitions by name for hierarchical search * @param name The name to search for * @returns Array of element or type definitions matching the name */ getGlobalElementOrTypeDefs(name) { const defs = []; const seenNodes = new Set(); // Check global elements if (this.schemaIndex.elements[name]) { for (const element of this.schemaIndex.elements[name]) { if (!seenNodes.has(element)) { defs.push(element); seenNodes.add(element); } } } // Check global types if (this.schemaIndex.types[name]) { const typeElement = this.schemaIndex.types[name]; if (!seenNodes.has(typeElement)) { defs.push(typeElement); seenNodes.add(typeElement); } } // For hierarchical search, we also need to include elements from elementMap // but ONLY when this method is called from hierarchical search context // The elementMap contains elements that may be reachable through hierarchy const mapDefs = this.elementMap[name] || []; for (const mapDef of mapDefs) { if (!seenNodes.has(mapDef.node)) { defs.push(mapDef.node); seenNodes.add(mapDef.node); } } return defs; } /** * Find element definitions within a parent definition by element name * @param parentDef The parent element or type definition to search in * @param elementName The name of the element to find * @returns Array of matching element definitions */ findElementsInDefinition(parentDef, elementName) { if (!parentDef) return []; const ns = 'xs:'; const results = []; let maxSearchDepth = 0; // Get the actual type definition to search in let typeNode = parentDef; // If parentDef is an element, get its type if (parentDef.nodeName === ns + 'element') { const typeName = parentDef.getAttribute('type'); if (typeName && this.schemaIndex.types[typeName]) { typeNode = this.schemaIndex.types[typeName]; } else { // Look for inline complexType for (let i = 0; i < parentDef.childNodes.length; i++) { const child = parentDef.childNodes[i]; if (child.nodeType === 1 && child.nodeName === ns + 'complexType') { typeNode = child; break; } } } } // Search recursively through the type definition with optimizations const visited = new Set(); const searchInNode = (node, depth = 0) => { if (!node || node.nodeType !== 1) return; // Track maximum search depth for metrics maxSearchDepth = Math.max(maxSearchDepth, depth); // Depth limit for performance - prevent excessive recursion if (depth > 20) return; // Use the actual DOM node reference for cycle detection to avoid false positives // This ensures we only skip when we encounter the exact same node again if (visited.has(node)) return; visited.add(node); // Check if this is the element we're looking for if (node.nodeName === ns + 'element' && node.getAttribute('name') === elementName) { results.push(node); return; // Don't recurse into found elements - early exit optimization } // Early exit optimization: if we've found enough results, stop searching if (results.length >= 3) return; // Handle type references and extensions if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = this.schemaIndex.types[baseName]; if (baseType) { searchInNode(baseType, depth + 1); } } // Handle group references if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refName = node.getAttribute('ref'); const groupDef = this.schemaIndex.groups[refName]; if (groupDef) { searchInNode(groupDef, depth + 1); } } // Handle structural elements - recurse into ALL children if (node.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all' || node.nodeName === ns + 'complexType' || node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent' || node.nodeName === ns + 'group') { // For structural nodes, recursively search all children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { searchInNode(child, depth + 1); // Early exit if we found enough results if (results.length >= 3) break; } } } }; searchInNode(typeNode); return results; } /** * Find ALL immediate child elements within a parent definition (without filtering by name) * This is similar to findElementsInDefinition but returns all direct child elements * @param parentDef The parent element or type definition to search in * @returns Array of all immediate child element definitions */ findAllElementsInDefinition(parentDef) { if (!parentDef) return []; const ns = 'xs:'; const results = []; // Get the actual type definition to search in let typeNode = parentDef; // If parentDef is an element, get its type if (parentDef.nodeName === ns + 'element') { const typeName = parentDef.getAttribute('type'); if (typeName && this.schemaIndex.types[typeName]) { typeNode = this.schemaIndex.types[typeName]; } else { // Look for inline complexType for (let i = 0; i < parentDef.childNodes.length; i++) { const child = parentDef.childNodes[i]; if (child.nodeType === 1 && child.nodeName === ns + 'complexType') { typeNode = child; break; } } } } // Search for IMMEDIATE child elements (limited depth for performance) const visited = new Set(); const searchInNode = (node, depth = 0) => { if (!node || node.nodeType !== 1) return; // Depth limit for performance - prevent excessive recursion if (depth > 20) { return; } // Use the actual DOM node reference for cycle detection if (visited.has(node)) return; visited.add(node); // If this is an element definition, add it to results if (node.nodeName === ns + 'element' && node.getAttribute('name')) { results.push(node); return; // Don't recurse into found elements - we only want immediate children } // Handle type references and extensions if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { const baseName = node.getAttribute('base'); const baseType = this.schemaIndex.types[baseName]; if (baseType) { searchInNode(baseType, depth + 1); } } // Handle group references if (node.nodeName === ns + 'group' && node.getAttribute('ref')) { const refName = node.getAttribute('ref'); const groupDef = this.schemaIndex.groups[refName]; if (groupDef) { searchInNode(groupDef, depth + 1); } } // Handle structural elements - recurse into ALL children if (node.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all' || node.nodeName === ns + 'complexType' || node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent' || node.nodeName === ns + 'group') { // For structural nodes, recursively search all children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { searchInNode(child, depth + 1); } } } }; searchInNode(typeNode, 0); return results; } /** * Get enhanced attribute information including type and validation details */ getElementAttributes(elementName, hierarchy = []) { // STRICT HIERARCHY RULE: If hierarchy provided, only search in hierarchy context // Never fall back to global elements when hierarchy is specified // If no hierarchy provided, only search global elements if (hierarchy.length === 0) { return this.getElementAttributesWithHierarchy(elementName, []); } // Step 1: Use progressive hierarchy search to find attributes // Stop as soon as we find any attributes at any depth // for (let contextDepth = 1; contextDepth <= hierarchy.length; contextDepth++) { // // Take the first contextDepth elements (immediate context) // const currentContext = hierarchy.slice(0, contextDepth); // const contextAttrs = this.getElementAttributesWithHierarchy(elementName, currentContext); const contextAttrs = this.getElementAttributesWithHierarchy(elementName, hierarchy); if (contextAttrs.length > 0) { // Found attributes at this depth - return them immediately return contextAttrs; } // } // No attributes found at any depth - do NOT fall back to global search return []; } /** * Get element attributes with hierarchy context for internal use * @param elementName The element name to get attributes for * @param hierarchy The element hierarchy in bottom-up order * @returns Array of attribute information */ getElementAttributesWithHierarchy(elementName, hierarchy) { // Create cache key const cacheKey = `attrs:${elementName}:${hierarchy.join('>')}`; // Check cache first if (this.cache.attributeCache.has(cacheKey)) { return this.cache.attributeCache.get(cacheKey); } const attributes = {}; // Get the correct element definition based on hierarchical context const def = this.getElementDefinition(elementName, hierarchy); if (!def) { // Cache empty result const emptyResult = []; this.cache.attributeCache.set(cacheKey, emptyResult); this.ensureCacheSize(); return emptyResult; } // Use the definition const bestDef = def; // Collect attributes from the element definition this.collectAttrs(bestDef, attributes); const result = Object.entries(attributes).map(([name, node]) => ({ name, node })); // Cache the result this.cache.attributeCache.set(cacheKey, result); this.ensureCacheSize(); return result; } /** * Recursively collect attributes from element and type definitions * @param node The current node to collect attributes from * @param attributes Record to accumulate found attributes * @param visited Set to track visited nodes and prevent infinite recursion */ collectAttrs(node, attributes, visited = new Set()) { if (!node || node.nodeType !== 1) return; const ns = 'xs:'; // Use a unique key for types/groups to avoid infinite recursion let key = null; if (node.nodeName === ns + 'complexType' && node.getAttribute('name')) { key = 'type:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'group' && node.getAttribute('name')) { key = 'group:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('name')) { key = 'attrgroup:' + node.getAttribute('name'); } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('ref')) { key = 'attrgroupref:' + node.getAttribute('ref'); } if (key && visited.has(key)) return; if (key) visited.add(key); // Handle different node types if (node.nodeName === ns + 'attribute') { const name = node.getAttribute('name'); if (name) { attributes[name] = node; } } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('ref')) { // Attribute group reference - resolve the reference const refName = node.getAttribute('ref'); const group = this.schemaIndex.attributeGroups[refName]; if (group) { this.collectAttrs(group, attributes, visited); } } else if (node.nodeName === ns + 'attributeGroup' && node.getAttribute('name')) { // Named attribute group definition - process its children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child, attributes, visited); } } } else if (node.nodeName === ns + 'extension' && node.getAttribute('base')) { // Type extension - inherit from base and add own attributes const baseName = node.getAttribute('base'); const base = this.schemaIndex.types[baseName]; if (base) { this.collectAttrs(base, attributes, visited); } // Also process the extension's own attributes and attribute groups for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child, attributes, visited); } } } else if (node.nodeName === ns + 'complexContent' || node.nodeName === ns + 'simpleContent') { // Content wrapper - process children for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1) { this.collectAttrs(child, attributes, visited); } } } else if (node.nodeName === ns + 'complexType' || node.nodeName === ns + 'sequence' || node.nodeName === ns + 'choice' || node.nodeName === ns + 'all') { // Structural nodes - traverse children but skip nested element definitions for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1 && child.nodeName !== ns + 'element') { this.collectAttrs(child, attributes, visited); } } } // Handle type reference const typeName = node.getAttribute('type'); if (typeName && this.schemaIndex.types[typeName]) { this.collectAttrs(this.schemaIndex.types[typeName], attributes, visited); } // Handle inline complexType for (let i = 0; i < node.childNodes.length; i++) { const child = node.childNodes[i]; if (child.nodeType === 1 && child.nodeName === ns + 'complexType') { this.collectAttrs(child, attributes, visited); break; } } } /** * Get enhanced attribute information including type and validation details */ getElementAttributesWithTypes(elementName, hierarchy = []) { const attributes = this.getElementAttributes(elementName, hierarchy); // Enhance each attribute with type information return attributes.map(attr => { const enhancedAttr = { name: attr.name, type: attr.node.getAttribute('type') || undefined, required: attr.node.getAttribute('use') === 'required' }; // Extract attribute's own annotation const annotation = Schema.extractAnnotationText(attr