salve-annos
Version:
A fork with support for documentation of Salve, a Javascript library which implements a validator able to validate an XML document on the basis of a subset of RelaxNG.
195 lines • 8.18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.step16 = step16;
/**
* Simplification step 16.
* @author Louis-Dominique Dubeau
* @license MPL 2.0
* @copyright 2013, 2014 Mangalam Research Center for Buddhist Languages
*/
const parser_1 = require("../parser");
const schema_validation_1 = require("../schema-validation");
const util_1 = require("./util");
// Elements that cannot contain references.
const skip = new Set(["name", "anyName", "nsName", "param", "empty",
"text", "value", "notAllowed", "ref"]);
function wrapElements(state, root) {
let elementCount = 0;
const { seenRefs } = state;
const toAppend = [];
for (const el of (0, util_1.findDescendantsByLocalName)(root, "element")) {
elementCount++;
// If an element is not appearing in a define element, then create one for
// it.
// tslint:disable-next-line:no-non-null-assertion
if (el.parent.local !== "define") {
// The first child of an ``element`` element is a name class but not
// necessarily ``name``.
const first = el.children[0];
const elName = first.local === "name" ? first.text : "";
// Note that elName in the following string is not necessary to guarantee
// uniqueness. It is a convenience that allows recognizing names more
// easily. So the value ``""`` used when we don't have a name element does
// not harm the uniqueness of the name.
const name = `__${elName}-elt-${elementCount}`;
const ref = parser_1.Element.makeElement("ref", []);
ref.setAttribute("name", name);
el.parent.replaceChildWith(el, ref);
seenRefs.add(name);
const defEl = parser_1.Element.makeElement("define", [el]);
toAppend.push(defEl);
defEl.setAttribute("name", name);
}
}
return toAppend;
}
function removeDefsWithoutElement(state, el) {
// A define which does not contain an ``element`` child is going to be
// removed. Any reference to it will have to be replaced with the content of
// the ``define``.
const removedDefines = state.removedDefines;
const children = el.children;
// We always keep ``start``.
const keep = [children[0]];
// The el parameter is the grammar. By this stage, it has a ``start`` element
// as its first child, and ``define`` elements for the remainder of its
// children.
for (let ix = 1; ix < children.length; ++ix) {
const child = children[ix];
// Define elements by this time have a single child, which is an
// element (but may not be an ``element`` element).
const topElement = child.children[0];
if (topElement.local === "element") {
keep.push(child);
continue;
}
child.removeChildAt(0); // Remove topElement from child.
removedDefines.set(child.mustGetAttribute("name"), { topElement, used: false });
}
el.replaceContent(keep);
}
/**
* Substitute ``ref`` elements that point to ``define`` elements that have been
* removed due to not containing a top-level ``element`` with the content of the
* referred define.
*
* @param state The transformation state.
*
* @param el The element to process.
*
* @returns A replacement for the element, which may be equal to ``el`` if there
* is no replacement.
*/
function substituteRefs(state, el, seenNames) {
const local = el.local;
let ret = el;
if (local === "ref") {
// If a reference is to a definition that does not contain an element
// element as the top element, move the definition in place of the ref.
const name = el.mustGetAttribute("name");
if (seenNames.has(name)) {
throw new schema_validation_1.SchemaValidationError(`circularity on the definition named ${name}`);
}
const def = state.removedDefines.get(name);
if (def === undefined) {
// We are keeping this reference, so mark it as seen. Otherwise, we're
// going to remove it, and we don't need to mark it.
state.seenRefs.add(name);
}
else {
// If the definition was used, clone it to allow for multiple copies of
// the definition's content to be put into the tree if it is references
// multiple times.
if (def.used) {
ret = def.topElement.clone();
}
else {
// Walk the element we're about to put into the tree. We walk it only
// once, and record the result of walking it.
const newNames = new Set(seenNames);
newNames.add(name);
def.topElement = ret = substituteRefs(state, def.topElement, newNames);
def.used = true;
}
}
}
else if (!skip.has(local)) {
//
// By this point, the majority of elements have at most two children.
// (<grammar> is the exception.)
//
// Due to the !skip.has(local) test above, first, and second are Element
// object, if they exists. So we assert instead of testing.
//
const [first, second] = el.children;
if (first !== undefined) {
const sub1 = substituteRefs(state, first, seenNames);
if (first !== sub1) {
el.replaceChildAt(0, sub1);
}
if (second !== undefined) {
const sub2 = substituteRefs(state, second, seenNames);
if (second !== sub2) {
el.replaceChildAt(1, sub2);
}
}
}
}
return ret;
}
/**
* Implements step 16 of the XSL pipeline. Namely:
*
* - All ``element`` elements that are not wrapped in a ``define`` element are
* wrapped in new ``define`` elements. And a ``ref`` element takes the
* place of the original ``element`` and refers to the new ``define``.
*
* - ``ref`` elements that reference a ``define`` which does not contain an
* ``element`` element as the top element are replaced by the contents of the
* ``define`` element they reference.
*
* - Remove ``define`` elements that are not referenced.
*
* @param tree The tree to process. It is modified in-place.
*
* @returns The new root of the tree.
*/
function step16(tree) {
const currentTree = tree;
if (currentTree.local !== "grammar") {
throw new Error("must be called with a grammar element");
}
const state = {
// By this point the top element must be the only grammar in the tree.
removedDefines: new Map(),
seenRefs: new Set(),
};
// The specification is not super clear about this, but we have to perform the
// wrapping of ``element`` in ``define`` before starting with ref substitution
// because ref subsitution is liable to get into an infinite loop where
// ``define/@name="x"`` contains a ``ref/@name="x"``. By doing the element
// wrapping first, we eliminate those cases that are valid Relax NG. Any
// remaining ``define`` which is without a top-level ``element`` and is
// self-referential is invalid.
const toAppend = wrapElements(state, currentTree);
// We wait until appending the new definitions so that the following operation
// does not have to scan through them needlessly. The new definitions contain
// ``element`` as their top pattern so they cannot be removed.
removeDefsWithoutElement(state, currentTree);
currentTree.appendChildren(toAppend);
const seenNames = new Set();
const { children } = currentTree;
for (let ix = 0; ix < children.length; ++ix) {
const child = children[ix];
if (!(0, parser_1.isElement)(child)) {
continue;
}
const substitute = substituteRefs(state, child, seenNames);
if (child !== substitute) {
currentTree.replaceChildAt(ix, substitute);
}
}
(0, util_1.removeUnreferencedDefs)(currentTree, state.seenRefs);
return currentTree;
}
//# sourceMappingURL=step16.js.map