typesxml
Version:
Open source XML library written in TypeScript
571 lines • 21.7 kB
JavaScript
/*******************************************************************************
* 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
*******************************************************************************/
import { existsSync } from "node:fs";
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { Constants } from "./Constants.js";
import { DOMBuilder } from "./DOMBuilder.js";
import { SAXParser } from "./SAXParser.js";
import { XMLAttribute } from "./XMLAttribute.js";
import { XMLElement } from "./XMLElement.js";
export class RelaxNGParser {
catalog;
baseDir;
root;
defaultPrefix = "";
defaultNamespace = Constants.RELAXNG_NS_URI;
definitions = new Map();
elements = [];
divsRemoved = false;
constructor(schemaPath, catalog) {
const absolutePath = isAbsolute(schemaPath) ? schemaPath : resolve(schemaPath);
this.baseDir = dirname(absolutePath);
const contentHandler = new DOMBuilder();
const parser = new 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.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.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.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.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.TEXT_NODE || nodeType === Constants.PROCESSING_INSTRUCTION_NODE) {
newContent.push(node);
continue;
}
if (nodeType === 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.TEXT_NODE) {
const textNode = node;
if (!this.isBlankText(textNode)) {
newContent.push(node);
}
continue;
}
if (nodeType === Constants.PROCESSING_INSTRUCTION_NODE) {
newContent.push(node);
continue;
}
if (nodeType === 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.TEXT_NODE) {
const textNode = node;
if (!this.isBlankText(textNode)) {
newContent.push(node);
}
continue;
}
if (nodeType === Constants.PROCESSING_INSTRUCTION_NODE) {
newContent.push(node);
continue;
}
if (nodeType === 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.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("ns", nsAttr.getValue()));
element.removeAttribute("ns");
}
else {
const resolvedNamespace = this.resolveNamespaceBinding(nameValue, currentContext, isElementPattern, isAttributePattern);
if (resolvedNamespace) {
nameElement.setAttribute(new 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 = 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 = fileURLToPath(normalized);
}
catch {
return undefined;
}
}
if (isAbsolute(normalized)) {
return existsSync(normalized) ? normalized : undefined;
}
const resolved = resolve(this.baseDir, normalized);
return existsSync(resolved) ? resolved : undefined;
}
createRelaxNGElement(localName) {
const qualifiedName = this.defaultPrefix ? `${this.defaultPrefix}:${localName}` : localName;
return new 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.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.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;
}
}
//# sourceMappingURL=RelaxNGParser.js.map