UNPKG

typesxml

Version:

Open source XML library written in TypeScript

575 lines 22.3 kB
"use strict"; /******************************************************************************* * Copyright (c) 2023-2026 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/epl-v10.html * * Contributors: * Maxprograms - initial API and implementation *******************************************************************************/ Object.defineProperty(exports, "__esModule", { value: true }); exports.RelaxNGParser = void 0; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const node_url_1 = require("node:url"); const Constants_js_1 = require("./Constants.js"); const DOMBuilder_js_1 = require("./DOMBuilder.js"); const SAXParser_js_1 = require("./SAXParser.js"); const XMLAttribute_js_1 = require("./XMLAttribute.js"); const XMLElement_js_1 = require("./XMLElement.js"); class RelaxNGParser { catalog; baseDir; root; defaultPrefix = ""; defaultNamespace = Constants_js_1.Constants.RELAXNG_NS_URI; definitions = new Map(); elements = []; divsRemoved = false; constructor(schemaPath, catalog) { const absolutePath = (0, node_path_1.isAbsolute)(schemaPath) ? schemaPath : (0, node_path_1.resolve)(schemaPath); this.baseDir = (0, node_path_1.dirname)(absolutePath); const contentHandler = new DOMBuilder_js_1.DOMBuilder(); const parser = new SAXParser_js_1.SAXParser(); if (catalog) { this.catalog = catalog; parser.setCatalog(this.catalog); } parser.setContentHandler(contentHandler); parser.parseFile(absolutePath); const documentRoot = contentHandler.getDocument()?.getRoot(); if (!documentRoot) { throw new Error(`RelaxNG schema "${absolutePath}" could not be parsed`); } this.root = documentRoot; this.defaultPrefix = this.getPrefix(this.root); const xmlnsDefault = this.root.getAttribute("xmlns"); if (xmlnsDefault) { this.defaultNamespace = xmlnsDefault.getValue(); } else if (this.defaultPrefix) { const prefixedNs = this.root.getAttribute(`xmlns:${this.defaultPrefix}`); if (prefixedNs) { this.defaultNamespace = prefixedNs.getValue(); } } if (!this.defaultNamespace) { this.defaultNamespace = Constants_js_1.Constants.RELAXNG_NS_URI; } this.removeForeign(this.root); this.replaceExternalRef(this.root); this.replaceIncludes(this.root); do { this.divsRemoved = false; this.removeDivs(this.root); } while (this.divsRemoved); this.nameAttribute(this.root, new Map()); } getElements() { const result = new Map(); this.definitions = new Map(); this.harvestDefinitions(this.root); this.elements = []; this.harvestElements(this.root); for (const element of this.elements) { const nameElement = this.findChildByLocalName(element, "name"); if (!nameElement) { continue; } const elementInfo = this.extractNameInfo(nameElement); if (!elementInfo) { continue; } const defaults = new Map(); const visitedRefs = new Set(); this.collectAttributeDefaultsFromPattern(element, defaults, visitedRefs, true); if (defaults.size === 0) { continue; } this.storeElementDefaults(result, elementInfo, defaults); } return result; } storeElementDefaults(result, elementInfo, defaults) { result.set(elementInfo.lexicalName, this.cloneAttributeDefaultMap(defaults)); if (!result.has(elementInfo.localName)) { result.set(elementInfo.localName, this.cloneAttributeDefaultMap(defaults)); } if (elementInfo.namespace) { const namespacedKey = this.buildAttributeKey(elementInfo.localName, elementInfo.namespace); result.set(namespacedKey, this.cloneAttributeDefaultMap(defaults)); } } cloneAttributeDefaultMap(source) { const clone = new Map(); source.forEach((value, key) => { clone.set(key, { localName: value.localName, namespace: value.namespace, lexicalName: value.lexicalName, value: value.value }); }); return clone; } collectAttributeDefaultsFromPattern(pattern, defaults, visitedRefs, allowElementTraversal) { const localName = this.getLocalNameFromElement(pattern); if (localName === "attribute") { this.addAttributeDefault(pattern, defaults); return; } if (localName === "ref" || localName === "parentRef") { const nameAttr = pattern.getAttribute("name"); const refName = nameAttr?.getValue(); if (!refName || visitedRefs.has(refName)) { return; } visitedRefs.add(refName); const referenced = this.definitions.get(refName); if (referenced) { this.collectAttributeDefaultsFromPattern(referenced, defaults, visitedRefs, allowElementTraversal); } return; } let childAllowTraversal = allowElementTraversal; if (localName === "element") { if (!allowElementTraversal) { return; } childAllowTraversal = false; } for (const child of pattern.getChildren()) { if (child.getNodeType() !== Constants_js_1.Constants.ELEMENT_NODE) { continue; } this.collectAttributeDefaultsFromPattern(child, defaults, visitedRefs, childAllowTraversal); } } addAttributeDefault(attributeElement, defaults) { const defaultValue = this.findDefaultValue(attributeElement); if (defaultValue === undefined) { return; } const nameElement = this.findChildByLocalName(attributeElement, "name"); if (!nameElement) { return; } const nameInfo = this.extractNameInfo(nameElement); if (!nameInfo) { return; } const attributeDefault = { localName: nameInfo.localName, namespace: nameInfo.namespace, lexicalName: nameInfo.lexicalName, value: defaultValue }; this.setAttributeDefault(defaults, attributeDefault); } extractNameInfo(nameElement) { const lexicalName = nameElement.getText().trim(); if (!lexicalName) { return undefined; } const nsAttr = nameElement.getAttribute("ns"); let namespace = nsAttr ? nsAttr.getValue() : undefined; let localName = lexicalName; const separatorIndex = lexicalName.indexOf(":"); if (separatorIndex !== -1) { localName = lexicalName.substring(separatorIndex + 1); if (!namespace) { const prefix = lexicalName.substring(0, separatorIndex); if (prefix === "xml") { namespace = "http://www.w3.org/XML/1998/namespace"; } } } return { lexicalName: lexicalName, localName: localName, namespace: namespace && namespace.length > 0 ? namespace : undefined }; } findDefaultValue(attribute) { for (const attr of attribute.getAttributes()) { if (this.getLocalNameFromString(attr.getName()) === "defaultValue") { return attr.getValue(); } } return this.findDefaultValueFromChildren(attribute); } findDefaultValueFromChildren(attribute) { // Search depth-first for any compatibility "defaultValue" element among descendants const stack = []; for (const child of attribute.getChildren()) { if (child.getNodeType() === Constants_js_1.Constants.ELEMENT_NODE) { stack.push(child); } } while (stack.length > 0) { const node = stack.pop(); if (this.getLocalNameFromElement(node) === "defaultValue") { return node.getText().trim(); } for (const child of node.getChildren()) { if (child.getNodeType() === Constants_js_1.Constants.ELEMENT_NODE) { stack.push(child); } } } return undefined; } setAttributeDefault(target, value) { const key = this.buildAttributeKey(value.localName, value.namespace); const removals = []; target.forEach((existing, existingKey) => { if (existing.localName !== value.localName) { return; } const sameNamespace = existing.namespace === value.namespace; if (sameNamespace && existingKey === key) { return; } if (sameNamespace || (value.namespace && !existing.namespace)) { removals.push(existingKey); } }); for (const removalKey of removals) { target.delete(removalKey); } target.set(key, { localName: value.localName, namespace: value.namespace, lexicalName: value.lexicalName, value: value.value }); } buildAttributeKey(name, namespace) { if (namespace) { return namespace + "|" + name; } return name; } removeForeign(element) { const newContent = []; for (const node of element.getContent()) { const nodeType = node.getNodeType(); if (nodeType === Constants_js_1.Constants.TEXT_NODE || nodeType === Constants_js_1.Constants.PROCESSING_INSTRUCTION_NODE) { newContent.push(node); continue; } if (nodeType === Constants_js_1.Constants.ELEMENT_NODE) { const child = node; if (!this.isRelaxNGElement(child)) { if (this.isCompatibilityAnnotation(child)) { newContent.push(child); } continue; } this.removeForeign(child); newContent.push(child); } } element.setContent(newContent); } replaceExternalRef(element) { const newContent = []; for (const node of element.getContent()) { const nodeType = node.getNodeType(); if (nodeType === Constants_js_1.Constants.TEXT_NODE) { const textNode = node; if (!this.isBlankText(textNode)) { newContent.push(node); } continue; } if (nodeType === Constants_js_1.Constants.PROCESSING_INSTRUCTION_NODE) { newContent.push(node); continue; } if (nodeType === Constants_js_1.Constants.ELEMENT_NODE) { const child = node; if (this.getLocalNameFromElement(child) === "externalRef") { const hrefAttr = child.getAttribute("href"); const href = hrefAttr?.getValue() ?? ""; const resolved = this.resolveHref(href); if (!resolved) { throw new Error(`RelaxNG externalRef target not found: ${href}`); } const parser = new RelaxNGParser(resolved, this.catalog); newContent.push(parser.getRootElement()); continue; } this.replaceIncludes(child); newContent.push(child); } } element.setContent(newContent); } replaceIncludes(element) { const newContent = []; for (const node of element.getContent()) { const nodeType = node.getNodeType(); if (nodeType === Constants_js_1.Constants.TEXT_NODE) { const textNode = node; if (!this.isBlankText(textNode)) { newContent.push(node); } continue; } if (nodeType === Constants_js_1.Constants.PROCESSING_INSTRUCTION_NODE) { newContent.push(node); continue; } if (nodeType === Constants_js_1.Constants.ELEMENT_NODE) { const child = node; if (this.getLocalNameFromElement(child) === "include") { const hrefAttr = child.getAttribute("href"); const href = hrefAttr?.getValue() ?? ""; const resolved = this.resolveHref(href); if (!resolved) { throw new Error(`RelaxNG include target not found: ${href}`); } const parser = new RelaxNGParser(resolved, this.catalog); const div = this.createRelaxNGElement("div"); div.addElement(parser.getRootElement()); for (const includeChild of child.getChildren()) { div.addElement(includeChild); } newContent.push(div); continue; } this.replaceIncludes(child); newContent.push(child); } } element.setContent(newContent); } removeDivs(element) { const newContent = []; for (const node of element.getContent()) { if (node.getNodeType() === Constants_js_1.Constants.ELEMENT_NODE) { const child = node; if (this.getLocalNameFromElement(child) === "div") { newContent.push(...child.getContent()); this.divsRemoved = true; } else { newContent.push(child); } } else { newContent.push(node); } } element.setContent(newContent); for (const child of element.getChildren()) { this.removeDivs(child); } } harvestDefinitions(element) { if (this.getLocalNameFromElement(element) === "define") { const nameAttr = element.getAttribute("name"); const definitionName = nameAttr?.getValue(); if (definitionName) { if (this.definitions.has(definitionName)) { const existing = this.definitions.get(definitionName); const combined = [...existing.getContent(), ...element.getContent()]; existing.setContent(combined); } else { this.definitions.set(definitionName, element); } } } for (const child of element.getChildren()) { this.harvestDefinitions(child); } } harvestElements(element) { if (this.getLocalNameFromElement(element) === "element" && this.findChildByLocalName(element, "name")) { this.elements.push(element); } for (const child of element.getChildren()) { this.harvestElements(child); } } nameAttribute(element, context) { const currentContext = this.augmentNamespaceContext(context, element); const localName = this.getLocalNameFromElement(element); const isElementPattern = localName === "element"; const isAttributePattern = localName === "attribute"; if ((isElementPattern || isAttributePattern) && element.hasAttribute("name")) { const nameValue = element.getAttribute("name")?.getValue() ?? ""; const nameElement = this.createRelaxNGElement("name"); nameElement.addString(nameValue); const nsAttr = element.getAttribute("ns"); if (nsAttr) { nameElement.setAttribute(new XMLAttribute_js_1.XMLAttribute("ns", nsAttr.getValue())); element.removeAttribute("ns"); } else { const resolvedNamespace = this.resolveNamespaceBinding(nameValue, currentContext, isElementPattern, isAttributePattern); if (resolvedNamespace) { nameElement.setAttribute(new XMLAttribute_js_1.XMLAttribute("ns", resolvedNamespace)); } } element.removeAttribute("name"); const content = [nameElement, ...element.getContent()]; element.setContent(content); } for (const child of element.getChildren()) { this.nameAttribute(child, currentContext); } } augmentNamespaceContext(baseContext, element) { const updated = new Map(baseContext); for (const attribute of element.getAttributes()) { const attributeName = attribute.getName(); if (attributeName === "xmlns") { updated.set("", attribute.getValue()); continue; } if (attributeName.startsWith("xmlns:")) { const prefix = attributeName.substring(6); updated.set(prefix, attribute.getValue()); } } if (!updated.has("xml")) { updated.set("xml", "http://www.w3.org/XML/1998/namespace"); } return updated; } resolveNamespaceBinding(lexicalName, context, isElementPattern, isAttributePattern) { const separatorIndex = lexicalName.indexOf(":"); if (separatorIndex === -1) { if (isElementPattern) { return context.get("") ?? undefined; } if (isAttributePattern) { return undefined; } return context.get("") ?? undefined; } const prefix = lexicalName.substring(0, separatorIndex); return context.get(prefix); } resolveHref(href) { if (!href) { return undefined; } let candidate = href; if (candidate.startsWith("file://")) { try { candidate = (0, node_url_1.fileURLToPath)(candidate); } catch { return undefined; } } const attempts = []; if (this.catalog) { const systemMatch = this.catalog.matchSystem(candidate); const uriMatch = this.catalog.matchURI(candidate); attempts.push(systemMatch); attempts.push(uriMatch); } attempts.push(candidate); for (const attempt of attempts) { const normalized = this.normalizeResolvedPath(attempt); if (normalized) { return normalized; } } return undefined; } normalizeResolvedPath(pathCandidate) { if (!pathCandidate) { return undefined; } let normalized = pathCandidate; if (normalized.startsWith("file://")) { try { normalized = (0, node_url_1.fileURLToPath)(normalized); } catch { return undefined; } } if ((0, node_path_1.isAbsolute)(normalized)) { return (0, node_fs_1.existsSync)(normalized) ? normalized : undefined; } const resolved = (0, node_path_1.resolve)(this.baseDir, normalized); return (0, node_fs_1.existsSync)(resolved) ? resolved : undefined; } createRelaxNGElement(localName) { const qualifiedName = this.defaultPrefix ? `${this.defaultPrefix}:${localName}` : localName; return new XMLElement_js_1.XMLElement(qualifiedName); } isBlankText(node) { return node.getValue().trim().length === 0; } getLocalNameFromElement(element) { return this.getLocalNameFromString(element.getName()); } getLocalNameFromString(name) { const index = name.indexOf(":"); return index === -1 ? name : name.substring(index + 1); } getPrefix(element) { const name = element.getName(); const index = name.indexOf(":"); return index === -1 ? "" : name.substring(0, index); } isCompatibilityAnnotation(element) { const localName = this.getLocalNameFromElement(element); if (localName === "defaultValue") { return true; } return false; } isRelaxNGElement(element) { const prefix = this.getPrefix(element); if (this.defaultPrefix) { if (prefix !== this.defaultPrefix) { return false; } } else if (prefix) { return false; } const selfNs = element.getAttribute("xmlns"); if (selfNs) { const value = selfNs.getValue(); if (value && value !== this.defaultNamespace && value !== Constants_js_1.Constants.RELAXNG_NS_URI) { return false; } } if (this.defaultPrefix) { const prefixedNs = element.getAttribute(`xmlns:${this.defaultPrefix}`); if (prefixedNs) { const value = prefixedNs.getValue(); if (value && value !== this.defaultNamespace && value !== Constants_js_1.Constants.RELAXNG_NS_URI) { return false; } } } return true; } findChildByLocalName(element, localName) { for (const child of element.getChildren()) { if (this.getLocalNameFromElement(child) === localName) { return child; } } return undefined; } getRootElement() { return this.root; } } exports.RelaxNGParser = RelaxNGParser; //# sourceMappingURL=RelaxNGParser.js.map