wed
Version:
Wed is a schema-aware editor for XML documents.
306 lines • 12.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
define(["require", "exports", "merge-options", "./domutil", "./mode-loader", "./wed-options-validation"], function (require, exports, merge_options_1, domutil_1, mode_loader_1, wed_options_validation_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
merge_options_1 = __importDefault(merge_options_1);
/**
* A node for the mode tree.
*/
class ModeNode {
/**
* @param mode The mode that this node holds.
*
* @param editor The editor for which we are holding a mode.
*
* @param selector The selector that determines to what this modes apply. This
* selector must have been converted to operate in the GUI tree.
*
* @param submodes The submodes set for this mode.
*
* @param wedOptions The cleaned up wed options that pertain to the mode held
* by this node.
*/
constructor(mode, editor, selector, submodes, wedOptions) {
this.mode = mode;
this.editor = editor;
this.selector = selector;
this.submodes = submodes;
this.wedOptions = wedOptions;
}
/**
* Determines whether an element matched by the selector of this ``ModeNode``
* node in the GUI tree contains a node. If it does, this means that the mode
* that this ``ModeNode`` holds, or one of the submode, governs the node.
*
* @param parentScope The element from which the selector in this ``ModeNode``
* is interpreted.
*
* @param node A GUI node to test.
*
* @returns The element that represents the top of the mode's region of
* activity and contains ``node``. Returns ``null`` if no element contains the
* node.
*/
containingElement(parentScope, node) {
if (!parentScope.contains(node)) {
return null;
}
if (this.selector === "") {
return parentScope;
}
const regions = parentScope.querySelectorAll(this.selector);
for (const region of Array.from(regions)) {
if (region.contains(node)) {
return region;
}
}
return null;
}
reduceTopFirst(fn, initialValue) {
let value = fn(initialValue, this);
for (const submode of this.submodes) {
value = submode.reduceTopFirst(fn, value);
}
return value;
}
eachTopFirst(fn) {
fn(this);
for (const submode of this.submodes) {
submode.eachTopFirst(fn);
}
}
get attributeHidingSpecs() {
if (this._attributeHidingSpecs === undefined) {
const attributeHiding = this.wedOptions.attributes.autohide;
if (attributeHiding === undefined) {
// No attribute hiding...
this._attributeHidingSpecs = null;
}
else {
const method = attributeHiding.method;
if (method !== "selector") {
throw new Error(`unknown attribute hiding method: ${method}`);
}
const specs = {
elements: [],
};
for (const element of attributeHiding.elements) {
const copy = merge_options_1.default({}, element);
copy.selector =
domutil_1.toGUISelector(copy.selector, this.mode.getAbsoluteNamespaceMappings());
specs.elements.push(copy);
}
this._attributeHidingSpecs = specs;
}
}
return this._attributeHidingSpecs;
}
get decorator() {
if (this._decorator === undefined) {
this._decorator = this.mode.makeDecorator();
}
return this._decorator;
}
}
/**
* A tree containing the modes configured for the current editing session.
*/
class ModeTree {
/**
* @param editor The editor for which we are building this tree.
*
* @param option The ``mode`` option from the options passed to the wed
* instance. This object will construct a tree from this option.
*/
constructor(editor, option) {
this.editor = editor;
this.option = option;
this.loader = new mode_loader_1.ModeLoader(editor, editor.runtime);
}
/**
* Load the modes, initialize them and build the tree.
*
* @returns A promise that resolves to ``this`` once all the modes are loaded
* and initialized.
*/
init() {
return __awaiter(this, void 0, void 0, function* () {
const combinedErrors = [];
this.root = yield this.makeNodes("", this.option, (path, errors) => {
for (const error of errors) {
combinedErrors.push(`mode at path ${path} has an error in its wed options: ${error}`);
}
});
if (combinedErrors.length > 0) {
throw new Error(`wed options are incorrect: ${combinedErrors.join("")}`);
}
return this;
});
}
/**
* Make the nodes of the tree. This function operates recursively: it will
* inspect ``option`` for a ``submode`` option and will call itself to create
* the necessary child nodes.
*
* @param selector The selector associated with the options passed in the 2nd
* argument.
*
* @param option The mode option being processed.
*
* @param errorHanler The handler to call on errors in processing the wed
* options. If this handler is called at all, then the returned value should
* not be used. We operate this way because we want to report all errors that
* can be reported, rather than abort early.
*
* @returns A promise that resolves to the created node.
*/
makeNodes(selector, option, errorHandler) {
return __awaiter(this, void 0, void 0, function* () {
const submode = option.submode;
const mode = yield this.loader.initMode(option.path, option.options);
const submodes = (submode !== undefined) ?
[yield this.makeNodes(domutil_1.toGUISelector(submode.selector, mode.getAbsoluteNamespaceMappings()), submode.mode, errorHandler)] :
[];
const rawOptions = mode.getWedOptions();
const result = wed_options_validation_1.processWedOptions(rawOptions);
let cleanedOptions;
if (Array.isArray(result)) {
errorHandler(option.path, result);
// This is a lie.
cleanedOptions = rawOptions;
}
else {
cleanedOptions = result;
}
return new ModeNode(mode, this.editor, selector, submodes, cleanedOptions);
});
}
getMode(node) {
return this.getModeNode(node).mode;
}
getDecorator(node) {
return this.getModeNode(node).decorator;
}
getWedOptions(node) {
const modeNode = this.getModeNode(node);
return modeNode.wedOptions;
}
getAttributeHandling(node) {
return this.getWedOptions(node).attributes.handling;
}
getAttributeHidingSpecs(node) {
return this.getModeNode(node).attributeHidingSpecs;
}
/**
* Get the mode node that governs a node.
*
* @param The node we want to check. This must be a done in the data tree or
* the GUI tree.
*
* @returns The mode that governs the node.
*/
getModeNode(node) {
// Handle the trivial case where there is no submode first.
if (this.root.submodes.length === 0) {
return this.root;
}
if (domutil_1.contains(this.editor.dataRoot, node)) {
const data = this.editor.fromDataNode(node);
if (data !== null) {
node = data;
}
}
if (!this.editor.guiRoot.contains(node)) {
throw new Error("did not pass a node in the GUI or data tree");
}
const result = this._getModeNode(this.root, this.editor.guiRoot, node);
if (result === undefined) {
throw new Error("cannot find a mode for the node; something is wrong");
}
return result;
}
_getModeNode(parent, parentScope, node) {
const scope = parent.containingElement(parentScope, node);
if (scope !== null) {
let narrower;
for (const submode of parent.submodes) {
narrower = this._getModeNode(submode, scope, node);
if (narrower !== undefined) {
return narrower;
}
}
return parent;
}
return undefined;
}
getStylesheets() {
return Object.keys(this.root.reduceTopFirst((accumulator, node) => {
for (const sheet of node.mode.getStylesheets()) {
accumulator[sheet] = true;
}
return accumulator;
}, Object.create(null)));
}
getMaxLabelLevel() {
return this.maxLabelLevelNode.wedOptions.label_levels.max;
}
getInitialLabelLevel() {
return this.maxLabelLevelNode.wedOptions.label_levels.initial;
}
/**
* The node with the maximum label visibility level. If multiple nodes have
* the same value, the earlier node "wins", and is the one provided by this
* property. For instance, if the root node and its submode have the same
* number, then this property has the root node for value.
*
* This is a cached value, computed on first access.
*/
get maxLabelLevelNode() {
if (this.cachedMaxLabelNode === undefined) {
this.cachedMaxLabelNode = this.root.reduceTopFirst((accumulator, node) => {
const accMax = accumulator.wedOptions.label_levels.max;
const nodeMax = node.wedOptions.label_levels.max;
return (nodeMax > accMax) ? node : accumulator;
}, this.root);
}
return this.cachedMaxLabelNode;
}
getValidators() {
return this.root.reduceTopFirst((accumulator, node) => {
const validator = node.mode.getValidator();
return validator !== undefined ?
accumulator.concat(validator) : accumulator;
}, []);
}
/**
* Call on each decorator to add its event handlers.
*/
addDecoratorHandlers() {
this.root.eachTopFirst((node) => {
node.decorator.addHandlers();
});
}
/**
* Call on each decorator to start listening.
*/
startListening() {
this.root.eachTopFirst((node) => {
node.decorator.startListening();
});
}
}
exports.ModeTree = ModeTree;
});
// LocalWords: MPL submodes submode combinedErrors nd preprocessed
// LocalWords: stylesheets
//# sourceMappingURL=mode-tree.js.map