wed
Version:
Wed is a schema-aware editor for XML documents.
260 lines • 12.5 kB
JavaScript
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
define(["require", "exports", "salve", "wed"], function (require, exports, salve, wed_1) {
/**
* Transformation registry for the generic mode.
* @author Louis-Dominique Dubeau
* @license MPL 2.0
* @copyright Mangalam Research Center for Buddhist Languages
*/
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
salve = __importStar(salve);
const { insertElement, unwrap, wrapInElement } = wed_1.transformation;
var Transformation = wed_1.transformation.Transformation;
const { childByClass, indexOf } = wed_1.domutil;
const { isAttr, isElement } = wed_1.domtypeguards;
function errFilter(err) {
const errMsg = err.error.toString();
return errMsg.lastIndexOf("tag required: ", 0) === 0;
}
/**
* Perform the autoinsertion algorithm on an element.
*
* @param el The element that should be subject to the autoinsertion algorithm.
*
* @param editor The editor which owns the element.
*/
function _autoinsert(el, editor) {
// tslint:disable-next-line:no-constant-condition strict-boolean-expressions
while (true) {
let errors = editor.validator.getErrorsFor(el);
errors = errors.filter(errFilter);
if (errors.length === 0) {
break;
}
const ename = errors[0].error.getNames()[0];
const names = ename.toArray();
// If names is null the pattern is not simple and we cannot autoinsert. If
// there is more than one option, we also cannot autoinsert.
if (names === null || names.length > 1) {
break;
}
const name = names[0];
const locations = editor.validator.possibleWhere(el, new salve.Event("enterStartTag", name.ns, name.name));
if (locations.length !== 1) {
break;
}
const mode = editor.modeTree.getMode(el);
const unresolved = mode.getAbsoluteResolver().unresolveName(name.ns, name.name);
if (unresolved === undefined) {
throw new Error(`cannot unresolve {${name.ns}}${name.name}`);
}
const actions = mode.getContextualActions("insert", unresolved, el, locations[0]);
// Don't auto insert if it happens that the operation would be ambiguous
// (ie. if there is more than one way to insert the element).
if (actions.length !== 1) {
break;
}
// Don't auto insert if the operation needs input from the user.
if (actions[0].needsInput) {
break;
}
//
// We move the caret ourselves rather than using moveCaretTo. In this
// context, it does not matter because autoinsert is meant to be called by a
// transformation anyway.
//
editor.caretManager.setCaret(el, locations[0]);
actions[0].execute({ name: unresolved });
}
}
function executeInsert(editor, data) {
const caret = editor.caretManager.getDataCaret();
if (caret === undefined) {
throw new Error("inserting without a defined caret!");
}
const mode = editor.modeTree.getMode(caret.node);
const absoluteResolver = mode.getAbsoluteResolver();
const ename = absoluteResolver.resolveName(data.name);
if (ename === undefined) {
throw new Error(`cannot resolve ${data.name}`);
}
const unresolved = editor.validator.unresolveNameAt(caret.node, caret.offset, ename.ns, ename.name);
const el = insertElement(editor.dataUpdater, caret.node, caret.offset, ename.ns, data.name);
if (unresolved === undefined) {
// The namespace used by the element has not been defined yet. So we need to
// define it.
const prefix = absoluteResolver.prefixFromURI(ename.ns);
const name = (prefix === "") ? "xmlns" : `xmlns:${prefix}`;
// The next name is necessarily resolvable so we assert that it is not
// resolving to undefined.
const xmlnsURI = absoluteResolver.resolveName("xmlns:q").ns;
editor.dataUpdater.setAttributeNS(el, xmlnsURI, name, ename.ns);
}
let caretNode = el;
if (mode.getModeOptions().autoinsert) {
_autoinsert(el, editor);
// Set el to the deepest first child, so that the caret is put in the right
// position.
while (caretNode !== null) {
const child = caretNode.firstChild;
if (child === null) {
break;
}
caretNode = child;
}
}
editor.caretManager.setCaret(caretNode, 0);
}
function executeUnwrap(editor, data) {
const node = data.node;
if (!isElement(node)) {
throw new Error("node must be an element");
}
const parent = node.parentNode;
const index = indexOf(parent.childNodes, node);
unwrap(editor.dataUpdater, node);
editor.caretManager.setCaret(parent, index);
}
function executeWrap(editor, data) {
const sel = editor.caretManager.sel;
if (sel == null) {
throw new Error("wrap transformation called with undefined range");
}
if (sel.collapsed) {
throw new Error("wrap transformation called with collapsed range");
}
const [startCaret, endCaret] = sel.mustAsDataCarets();
const mode = editor.modeTree.getMode(startCaret.node);
const ename = mode.getAbsoluteResolver().resolveName(data.name);
if (ename === undefined) {
throw new Error(`cannot resolve ${data.name}`);
}
const el = wrapInElement(editor.dataUpdater, startCaret.node, startCaret.offset, endCaret.node, endCaret.offset, ename.ns, data.name);
const parent = el.parentNode;
editor.caretManager.setCaret(startCaret.make(parent, indexOf(parent.childNodes, el) + 1));
}
function executeWrapContent(editor, data) {
const toWrap = data.node;
if (!isElement(toWrap)) {
throw new Error("node must be an element");
}
const mode = editor.modeTree.getMode(toWrap);
const ename = mode.getAbsoluteResolver().resolveName(data.name);
if (ename === undefined) {
throw new Error(`cannot resolve ${data.name}`);
}
wrapInElement(editor.dataUpdater, toWrap, 0, toWrap, toWrap.childNodes.length, ename.ns, data.name);
}
function executeDeleteElement(editor, data) {
const node = data.node;
if (!isElement(node)) {
throw new Error("node must be an element");
}
const parent = node.parentNode;
const index = indexOf(parent.childNodes, node);
const guiLoc = editor.caretManager.fromDataLocation(node, 0);
// If the node we start with is an Element, then the node in guiLoc is
// necessarily an Element too.
if (!guiLoc.node.classList.contains("_readonly")) {
editor.dataUpdater.removeNode(node);
editor.caretManager.setCaret(parent, index);
}
}
function executeDeleteParent(editor, data) {
const node = data.node;
if (!isElement(node)) {
throw new Error("node must be an element");
}
const parent = node.parentNode;
const index = indexOf(parent.childNodes, node);
const guiLoc = editor.caretManager.mustFromDataLocation(node, 0);
// If the node we start with is an Element, then the node in guiLoc is
// necessarily an Element too.
if (!guiLoc.node.classList.contains("_readonly")) {
editor.dataUpdater.removeNode(node);
editor.caretManager.setCaret(parent, index);
}
}
function executeAddAttribute(editor, data) {
const node = data.node;
if (!isElement(node)) {
throw new Error("node must be an element");
}
const guiLoc = editor.caretManager.mustFromDataLocation(node, 0);
// If the node we start with is an Element, then the node in guiLoc is
// necessarily an Element too.
if (!guiLoc.node.classList.contains("_readonly")) {
editor.dataUpdater.setAttribute(node, data.name, "");
const attr = node.getAttributeNode(data.name);
editor.caretManager.setCaret(attr, 0);
}
}
function executeDeleteAttribute(editor, data) {
const node = data.node;
if (node == null || !isAttr(node)) {
throw new Error("node must be an attribute");
}
const element = node.ownerElement;
const caretManager = editor.caretManager;
const guiOwnerLoc = caretManager.mustFromDataLocation(element, 0);
// If the node we start with is an Element, then the node in guiOwnerLoc
// is necessarily an Element too.
const guiOwner = guiOwnerLoc.node;
if (!guiOwner.classList.contains("_readonly")) {
const encoded = node.name;
const startLabel = childByClass(guiOwner, "__start_label");
// An earlier version of this code relied on the order of attributes in the
// data tree. However, this order is not consistent from platform to
// platform. Using the order of attributes in the GUI is
// consistent. Therefore we go to the GUI to find the next attribute.
const values = startLabel.getElementsByClassName("_attribute_value");
// We have to get the parent node because fromDataLocation brings us to the
// text node that contains the value.
const guiNode = caretManager.mustFromDataLocation(node, 0).node.parentNode;
const index = indexOf(values, guiNode);
const nextGUIValue = values[index + 1];
const nextAttr = nextGUIValue != null ?
editor.toDataNode(nextGUIValue) : null;
editor.dataUpdater.setAttribute(element, encoded, null);
// We set the caret inside the next attribute, or if it does not exist,
// inside the label.
if (nextAttr !== null) {
editor.caretManager.setCaret(nextAttr, 0);
}
else {
editor.caretManager.setCaret(guiOwner.getElementsByClassName("_element_name")[0], 0);
}
}
}
/**
* @param forEditorAPI The editor for which to create transformations.
*/
function makeTagTr(forEditor) {
const ret = Object.create(null);
ret.insert = new Transformation(forEditor, "insert", "Create new <name>", executeInsert, { abbreviatedDesc: "" });
ret.unwrap = new Transformation(forEditor, "unwrap", "Unwrap the content of this element", executeUnwrap);
ret.wrap = new Transformation(forEditor, "wrap", "Wrap in <name>", executeWrap);
ret["wrap-content"] = new Transformation(forEditor, "wrap-content", "Wrap content in <name>", executeWrapContent);
ret["delete-element"] = new Transformation(forEditor, "delete-element", "Delete this element", executeDeleteElement);
ret["delete-parent"] = new Transformation(forEditor, "delete-parent", "Delete <name>", executeDeleteParent);
ret["add-attribute"] = new Transformation(forEditor, "add-attribute", "Add @<name>", executeAddAttribute);
ret["delete-attribute"] = new Transformation(forEditor, "delete-attribute", "Delete this attribute", executeDeleteAttribute);
ret["insert-text"] = new Transformation(forEditor, "insert-text", "Insert \"<name>\"", (editor, data) => {
editor.insertText(data.name);
});
ret.split = forEditor.splitNodeTr;
return ret;
}
exports.makeTagTr = makeTagTr;
});
// LocalWords: TransformationRegistry Mangalam MPL Dubeau autoinsertion ie el
// LocalWords: autoinsert enterStartTag moveCaretTo xmlns guiLoc readonly
// LocalWords: guiOwnerLoc fromDataLocation
//# sourceMappingURL=generic-tr.js.map