xsd-lookup
Version:
Multi-schema XSD lookup utility
1,022 lines • 183 kB
JavaScript
"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;