xsd-lookup
Version:
Multi-schema XSD lookup utility
1,014 lines • 114 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 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