UNPKG

xsd-lookup

Version:
1,022 lines 183 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 path = __importStar(require("path")); const url_1 = require("url"); const xmldom_1 = require("@xmldom/xmldom"); class Schema { constructor(xsdFilePath, includeFiles = []) { this.name = ''; this.methodTimings = {}; this.methodCalls = {}; this.maxCacheSize = 100000; this.shouldProfileCaches = false; this.shouldProfileMethods = false; // Initialize caches and metrics first this.initializeCaches(); this.initializeCacheStats(); this.shouldProfileCaches = ((process.env.XSDL_PROFILE_CACHES || '').trim() === '1'); this.shouldProfileMethods = ((process.env.XSDL_PROFILE_METHODS || '').trim() === '1'); this.name = path.basename(xsdFilePath); 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(); } /** * Initialize all cache structures with empty maps */ initializeCaches() { this.cache = { attributeCache: new WeakMap(), enhancedAttributesCache: new WeakMap(), elementDefinitionCache: new Map(), elementsInDefinitionCache: new WeakMap(), elementsInDefinitionByNameCache: new WeakMap(), contentModelCache: new WeakMap(), annotationCache: new WeakMap(), possibleChildrenResultCache: new WeakMap(), validChildCache: new WeakMap(), modelNextNamesCache: new WeakMap(), modelStartNamesCache: new WeakMap(), containsCache: new WeakMap(), validationsCache: new WeakMap() }; } initializeCacheStats() { const zero = () => ({ hits: 0, misses: 0, sets: 0 }); this.cacheStats = { attributeCache: zero(), enhancedAttributesCache: zero(), elementDefinitionCache: zero(), elementsInDefinitionByNameCache: zero(), elementsInDefinitionCache: zero(), contentModelCache: zero(), annotationCache: zero(), possibleChildrenResultCache: zero(), validChildCache: zero(), modelStartNamesCache: zero(), modelNextNamesCache: zero(), containsCache: zero(), validationsCache: zero(), }; } printCacheStats() { if (!this.shouldProfileCaches) return; const entries = Object.keys(this.cacheStats); const rows = entries .map((name) => { const c = this.cacheStats[name]; return { name: String(name), hits: c.hits, misses: c.misses, sets: c.sets }; }) .sort((a, b) => a.name.localeCompare(b.name)); const headers = ['Name', 'Hits', 'Misses', 'Sets']; const col1W = Math.max(headers[0].length, ...rows.map(r => r.name.length)); const col2W = Math.max(headers[1].length, ...rows.map(r => String(r.hits).length)); const col3W = Math.max(headers[2].length, ...rows.map(r => String(r.misses).length)); const col4W = Math.max(headers[3].length, ...rows.map(r => String(r.sets).length)); const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length)); const lines = []; lines.push(`=== XSD-Lookup Cache Stats for "${this.name}" ===`); lines.push(pad(headers[0], col1W) + ' ' + pad(headers[1], col2W) + ' ' + pad(headers[2], col3W) + ' ' + pad(headers[3], col4W)); lines.push('-'.repeat(col1W) + ' ' + '-'.repeat(col2W) + ' ' + '-'.repeat(col3W) + ' ' + '-'.repeat(col4W)); for (const r of rows) { lines.push(pad(r.name, col1W) + ' ' + pad(String(r.hits), col2W) + ' ' + pad(String(r.misses), col3W) + ' ' + pad(String(r.sets), col4W)); } // eslint-disable-next-line no-console console.log(lines.join('\n')); } // Method timing profiling profStart() { return (globalThis.performance?.now?.() ?? Date.now()); } profEnd(method, t0) { if (!this.shouldProfileMethods) return; const t1 = (globalThis.performance?.now?.() ?? Date.now()); const dt = t1 - t0; this.methodTimings[method] = (this.methodTimings[method] || 0) + dt; this.methodCalls[method] = (this.methodCalls[method] || 0) + 1; } printMethodStats() { if (!this.shouldProfileMethods) return; const rows = Object.entries(this.methodTimings) .map(([name, total]) => { const calls = this.methodCalls[name] || 0; const avg = calls > 0 ? total / calls : 0; return { name, total, calls, avg }; }) .sort((a, b) => b.total - a.total); if (rows.length === 0) return; const headers = ['Name', 'Total Time (ms)', 'Count Calls', 'Average Time (ms)']; const col1W = Math.max(headers[0].length, ...rows.map(r => r.name.length)); const col2W = Math.max(headers[1].length, ...rows.map(r => r.total.toFixed(3).length)); const col3W = Math.max(headers[2].length, ...rows.map(r => String(r.calls).length)); const col4W = Math.max(headers[3].length, ...rows.map(r => r.avg.toFixed(3).length)); const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length)); const lines = []; lines.push(`=== XSD-Lookup Method Profile for "${this.name}" ===`); lines.push(pad(headers[0], col1W) + ' ' + pad(headers[1], col2W) + ' ' + pad(headers[2], col3W) + ' ' + pad(headers[3], col4W)); lines.push('-'.repeat(col1W) + ' ' + '-'.repeat(col2W) + ' ' + '-'.repeat(col3W) + ' ' + '-'.repeat(col4W)); for (const r of rows) { lines.push(pad(r.name, col1W) + ' ' + pad(r.total.toFixed(3), col2W) + ' ' + pad(String(r.calls), col3W) + ' ' + pad(r.avg.toFixed(3), col4W)); } // eslint-disable-next-line no-console console.log(lines.join('\n')); } /** * Clear all caches by reinitializing them */ clearCache() { this.initializeCaches(); } /** * Ensure cache sizes don't exceed the maximum limit using simple LRU eviction */ ensureCacheSize() { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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)); } } finally { if (__profiling) { this.profEnd('ensureCacheSize', __t0); } } } /** * 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 __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { const xml = fs.readFileSync(filePath, 'utf8'); const doc = new xmldom_1.DOMParser().parseFromString(xml, 'application/xml'); // Pre-split file into lines for faster per-line operations const lines = xml.split(/\r\n|\r|\n/); const linesCount = lines.length; // Annotate all elements in this document with their source file path so callers can resolve origin try { const annotate = (node) => { if (!node) return; if (node.nodeType === 1) { const el = node; // Store as a non-XSD attribute to avoid interfering with schema semantics if (!el.getAttribute('data-source-file')) { el.setAttribute('data-source-file', filePath); } const anyEl = el; const line = anyEl.lineNumber ?? anyEl.line; const column = anyEl.columnNumber ?? anyEl.column; if (typeof line === 'number' && typeof column === 'number' && line >= 1 && line <= linesCount && column >= 1 && !el.getAttribute('start-tag-length')) { const lineStr = lines[line - 1]; if (column <= lineStr.length && lineStr[column - 1] === '<') { const closingTagIdx = lineStr.indexOf('>', column - 1); const length = closingTagIdx >= 0 ? closingTagIdx - (column - 1) + 1 : lineStr.length - (column - 1); if (typeof closingTagIdx === 'number') { el.setAttribute('start-tag-length', String(length)); } } } } // Recurse children // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < (node.childNodes ? node.childNodes.length : 0); i++) { annotate(node.childNodes[i]); } }; if (doc && doc.documentElement) annotate(doc.documentElement); } catch { // Best-effort; if annotation fails, we still return the parsed document } return doc; } finally { if (__profiling) { this.profEnd('loadXml', __t0); } } } /** * 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 __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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)); } } } finally { if (__profiling) { this.profEnd('mergeXsds', __t0); } } } /** * 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 * @returns Array of collected elements with their parent information */ collectElements(node, parentName, elements = []) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { if (!node) return elements; if (node.nodeType === 1) { const element = node; if (element.localName === 'element' && element.hasAttribute('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); } } return elements; } finally { if (__profiling) { this.profEnd('collectElements', __t0); } } } /** * Build a map of element names to their definitions and parent relationships * @returns Record mapping element names to arrays of their definitions */ buildElementMap() { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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; } finally { if (__profiling) { this.profEnd('buildElementMap', __t0); } } } /** * 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) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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; const elementName = element.getAttribute('name'); if (elementName) { if (element.localName === 'element') { if (!elements[elementName]) elements[elementName] = []; elements[elementName].push(element); } else if (element.localName === 'group') { groups[elementName] = element; } else if (element.localName === 'attributeGroup') { attributeGroups[elementName] = element; } else if (element.localName === 'complexType') { types[elementName] = element; } else if (element.localName === 'simpleType') { types[elementName] = 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; const elementName = element.getAttribute('name'); // Only collect types and groups, not nested elements if (elementName) { if (element.localName === 'group') { groups[elementName] = element; } if (element.localName === 'attributeGroup') { attributeGroups[elementName] = element; } if (element.localName === 'complexType' && elementName) { types[elementName] = element; } if (element.localName === 'simpleType' && elementName) { types[elementName] = 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 }; } finally { if (__profiling) this.profEnd('indexSchema', __t0); } } /** * Build comprehensive element contexts, including elements reachable through groups */ buildElementContexts(globalElements, groups, types) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { const elementContexts = {}; // 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); } // 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, elementContexts, groups, types, [elementName]); } } return elementContexts; } finally { if (__profiling) { this.profEnd('buildElementContexts', __t0); } } } /** * Extract all elements from a group and add them to element contexts */ extractElementsFromGroup(groupElement, groupName, elementContexts, groups, types, visitedGroups = new Set()) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { // 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.localName === '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.localName === '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, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.localName === '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.localName === '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, 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]); } finally { if (__profiling) { this.profEnd('extractElementsFromGroup', __t0); } } } /** * Extract elements from a group while maintaining parent context information */ extractElementsFromGroupWithParentContext(groupElement, groupName, elementContexts, groups, types, parentContext, visitedGroups = new Set()) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { // 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.localName === '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.localName === '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, parentContext, new Set(visitedGroups)); } } // Handle type extensions - extract elements from the base type if (node.localName === '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.localName === '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, 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); } finally { if (__profiling) { this.profEnd('extractElementsFromGroupWithParentContext', __t0); } } } /** * Extract all elements from a complex type and add them to element contexts */ extractElementsFromType(typeElement, typeName, elementContexts, groups, types, visitedTypes = new Set(), parentElementNames = []) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { // 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.localName === '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, new Set(), [elementName, ...currentParents]); } } } // If this is a group reference, extract elements from the group and mark with group membership if (node.localName === '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, immediateParent); } } // Handle type extensions - extract elements from the base type if (node.localName === '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, 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); } finally { if (__profiling) { this.profEnd('extractElementsFromType', __t0); } } } /** * Extract inline elements from a global element definition * This captures elements like param under params that are defined inline */ extractInlineElementsFromElement(parentElement, elementContexts, groups, types, initialParents = []) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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.localName === '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, 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.localName === 'group' && node.getAttribute('ref')) { const refGroupName = node.getAttribute('ref'); const refGroup = groups[refGroupName]; if (refGroup) { this.extractElementsFromGroupWithParentContext(refGroup, refGroupName, elementContexts, groups, types, currentParents); } } // Handle type extensions - extract elements from the base type if (node.localName === '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, 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); } finally { if (__profiling) { this.profEnd('extractInlineElements', __t0); } } } getElementDefinition(elementName, hierarchy = []) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { // 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)) { if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.hits++; const cached = this.cache.elementDefinitionCache.get(fullCacheKey); return cached; } if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.misses++; const elementsFromMap = this.elementMap[elementName]; if (elementsFromMap && elementsFromMap.length === 1) { const elementFromMap = elementsFromMap[0].node; this.cache.elementDefinitionCache.set(fullCacheKey, elementFromMap); if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++; this.ensureCacheSize(); return elementFromMap; } // 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) { let keyPrefix = ''; for (const segment of hierarchy) { keyPrefix += `|${segment}`; const fullKey = `${elementName}::${keyPrefix}`; const cachedElement = this.cache.elementDefinitionCache.get(fullKey); if (cachedElement) { if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.hits++; this.cache.elementDefinitionCache.set(fullCacheKey, cachedElement); if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++; 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); if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++; break; // Exit the loop since we found a match } } } // Cache the final result (even if undefined) this.cache.elementDefinitionCache.set(fullCacheKey, result); if (this.shouldProfileCaches) this.cacheStats.elementDefinitionCache.sets++; this.ensureCacheSize(); return result; } finally { if (__profiling) this.profEnd('getElementDefinition', __t0); } } /** * 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) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { // 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 []; } finally { if (__profiling) { this.profEnd('getGlobalElementDefinitions', __t0); } } } /** * 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 __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { 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; } finally { if (__profiling) { this.profEnd('getGlobalElementOrTypeDefs', __t0); } } } /** * 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) { const __profiling = this.shouldProfileMethods; const __t0 = __profiling ? this.profStart() : 0; try { if (!parentDef) return []; const cache = this.cache.elementsInDefinitionByNameCache; let cached; if (cache.has(parentDef)) { cached = cache.get(parentDef); if (cached && cached.has(elementName)) { if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionByNameCache.hits++; return cached.get(elementName); } } else { cached = new Map(); cache.set(parentDef, cached); } if (this.shouldProfileCaches) this.cacheStats.elementsInDefinitionByNameCache.misses++; 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.localName === '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.localName === '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.localName === '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.localName === '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.localName === '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.localName === 'sequence' || node.localName === 'choice' || node.localName === 'all' || node.localName === 'complexType' || node.localName === 'complexContent' || node.localName === 'simpleContent' || node.localName === 'group') { // For structural nodes, recursively search all children for (let i = 0;