UNPKG

wed

Version:

Wed is a schema-aware editor for XML documents.

635 lines 25 kB
/** * Various utilities for wed. * @author Louis-Dominique Dubeau * @license MPL 2.0 * @copyright Mangalam Research Center for Buddhist Languages */ define(["require", "exports", "diff"], function (require, exports, diff_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * Calculates the distance on the basis of two deltas. This would typically be * called with the difference of X coordinates and the difference of Y * coordinates. * * @param delta1 The first delta. * * @param delta2 The second delta. * * @returns The distance. */ function distFromDeltas(delta1, delta2) { return Math.sqrt(delta1 * delta1 + delta2 * delta2); } exports.distFromDeltas = distFromDeltas; /** * Measures the distance of a point from a rectangle. If the point is in the * rectangle or touches it, the distance is 0. In the nomenclature below, left * and right are on the X axis and top and bottom on the Y axis. * * @param x The x coordinate of the point. * * @param y The y coordinate of the point. * * @param left The left coordinate of the rectangle. * * @param top The top coordinate of the rectangle. * * @param right The right coordinate of the rectangle. * * @param bottom The bottom coordinate of the rectangle. * * @returns The distance. */ function distFromRect(x, y, left, top, right, bottom) { const topDelta = y - top; const leftDelta = x - left; const bottomDelta = y - bottom; const rightDelta = x - right; const above = topDelta < 0; const below = bottomDelta > 0; // Neologism used to avoid conflict with left above. const lefter = leftDelta < 0; const righter = rightDelta > 0; const deltaX = lefter ? leftDelta : (righter ? rightDelta : 0); const deltaY = above ? topDelta : (below ? bottomDelta : 0); return distFromDeltas(deltaX, deltaY); } exports.distFromRect = distFromRect; /** * Measures the absolute horizontal and vertical distances of a point from a * rectangle. If the point is in the rectangle or touches it, the distance is * 0. In the nomenclature below, left and right are on the X axis and top and * bottom on the Y axis. * * @param x The x coordinate of the point. * * @param y The y coordinate of the point. * * @param left The left coordinate of the rectangle. * * @param top The top coordinate of the rectangle. * * @param right The right coordinate of the rectangle. * * @param bottom The bottom coordinate of the rectangle. * * @returns The distance. */ function distsFromRect(x, y, left, top, right, bottom) { const topDelta = y - top; const leftDelta = x - left; const bottomDelta = y - bottom; const rightDelta = x - right; const above = topDelta < 0; const below = bottomDelta > 0; // Neologism used to avoid conflict with left above. const lefter = leftDelta < 0; const righter = rightDelta > 0; const deltaX = lefter ? leftDelta : (righter ? rightDelta : 0); const deltaY = above ? topDelta : (below ? bottomDelta : 0); return { x: Math.abs(deltaX), y: Math.abs(deltaY) }; } exports.distsFromRect = distsFromRect; /** * Escape character in CSS class that could cause trouble in CSS * selectors. *This is not a general solution.* It supports enough for the needs * of wed. * * @param cls The class * * @returns The escaped class. */ function escapeCSSClass(cls) { // We should investigate replacing this with CSS.escape whenever the spec for // that function becomes stable. return cls.replace(/([\][\\/!"#$%&'()*+,.:;<=>?@^`{|}~])/g, "\\$1"); } exports.escapeCSSClass = escapeCSSClass; /** * Get the original element name of a node created for wed's data tree. * * @param el The element whose name we want. * * @returns The name. */ function getOriginalName(el) { // The original name is the first class name of the element that was created. return el.classList[0]; } exports.getOriginalName = getOriginalName; /** * Makes a class string for a node in wed's data tree. The string is meant to be * used for the corresponding node in wed's GUI tree. * * @param name The original element name. * * @param namespaces The namespaces that are known. This is used to convert * element name prefixes to namespace URIs. * * @returns The class string. */ function classFromOriginalName(name, namespaces) { // Special case if we want to match all if (name === "*") { return "._real"; } let [prefix, localName] = name.split(":"); if (localName === undefined) { localName = prefix; prefix = ""; } const ns = namespaces[prefix]; if (ns === undefined) { throw new Error(`prefix ${prefix} is not defined in namespaces`); } // We do not output `.${escapeCSSClass(name)}` because that's redundant for a // search. return `._local_${escapeCSSClass(localName)}\ ._xmlns_${escapeCSSClass(ns)}._real`; } exports.classFromOriginalName = classFromOriginalName; /** * Convert a string to a sequence of char codes. Each char code will be preceded * by the character ``x``. The char codes are converted to hexadecimal. * * This is meant to be used by wed's internal code. * * @private * * @param str The string to encode. * * @returns The encoded string. */ function stringToCodeSequence(str) { let encoded = ""; for (const char of str) { encoded += `x${char.charCodeAt(0).toString(16)}`; } return encoded; } exports.stringToCodeSequence = stringToCodeSequence; const ENCODED_RE = /^(?:x[a-f0-9]+)+$/; /** * Convert a code sequence created with [[stringToCodeSequence]] to a string. * * This is meant to be used by wed's internal code. * * @private * * @param str The sequence to decode. * * @returns The decoded string. */ function codeSequenceToString(str) { if (!ENCODED_RE.test(str)) { throw new Error("badly encoded string"); } let decoded = ""; // We slice to skip the initial x, and not get a first part which is "". for (const code of str.slice(1).split("x")) { decoded += String.fromCharCode(parseInt(code, 16)); } return decoded; } exports.codeSequenceToString = codeSequenceToString; /** * Encode the difference between an original string, and a modified string. This * is a specialized function designed to handle the difference between the name * we want to set for an attribute, and the name that HTML actually records. * * This function records the difference as a series of steps to recover the * original string: * * - ``g[number]`` means take ``[number]`` characters from the modified string * as they are. * * - ``m[number]`` means remove ``[number]`` characters from the modified * string. * * - ``p[codes]`` means add the codes ``[codes]`` to the modified string. * * - ``u[number]`` means convert ``[number]`` characters from the modified * string to uppercase. * * This is meant to be used by wed's internal code. * * @private * * @param orig The original. * * @param modified The modified string. * * @returns The difference, encoded as a string. */ function encodeDiff(orig, modified) { let diff = ""; if (orig !== modified) { const results = diff_1.diffChars(modified, orig); const last = results[results.length - 1]; for (let ix = 0; ix < results.length; ++ix) { const result = results[ix]; if (result.added === true) { diff += `p${stringToCodeSequence(result.value)}`; } else if (result.removed === true) { const next = results[ix + 1]; if ((next !== undefined && next.added === true) && (result.value.toUpperCase() === next.value)) { diff += `u${result.value.length}`; ix++; } else { diff += `m${result.value.length}`; } } else { // We don't output this if it is last, as it is implied. if (result !== last) { diff += `g${result.value.length}`; } } } } return diff; } exports.encodeDiff = encodeDiff; const OP_RE = /^(?:p([xa-f0-9]+))|(?:[gmu](\d+))/; /** * Decode the diff produced with [[encodeDiff]]. * * This is meant to be used by wed's internal code. * * @private * * @param name The name, after encoding. * * @param diff The diff. * * @returns The decoded attribute name. */ function decodeDiff(name, diff) { if (diff === "") { return name; } let nameIndex = 0; let result = ""; while (diff.length > 0) { const match = diff.match(OP_RE); if (match !== null) { diff = diff.slice(match[0].length); const op = match[0][0]; switch (op) { case "g": case "m": case "u": const length = parseInt(match[2]); switch (op) { case "g": result += name.slice(nameIndex, nameIndex + length); break; case "u": result += name.slice(nameIndex, nameIndex + length).toUpperCase(); break; case "m": break; default: throw new Error(`internal error: unexpected op ${op}`); } nameIndex += length; break; case "p": result += codeSequenceToString(match[1]); break; default: throw new Error(`unexpected operator ${op}`); } } // Nothing matched if (match === null) { throw new Error(`cannot parse diff: ${diff}`); } } // It is implied that the rest of the name is added. result += name.slice(nameIndex); return result; } exports.decodeDiff = decodeDiff; /** * Transforms an attribute name from wed's data tree to the original attribute * name before the data was transformed for use with wed. This reverses the * transformation done with [[encodeAttrName]]. * * @param encoded The encoded name. * * @returns A structure containing the decoded name the optional qualifier. */ function decodeAttrName(encoded) { const match = /^data-wed-(.+)-([^-]*?)$/.exec(encoded); if (match === null) { throw new Error("malformed name"); } // tslint:disable-next-line:prefer-const let [, name, diff] = match; let qualifier; // qualifier if (name[0] === "-") { const parts = /^-(.+?)-(.+)$/.exec(name); if (parts === null) { throw new Error("malformed name"); } [, qualifier, name] = parts; } name = name.replace(/---/, ":").replace(/---(-+)/g, "--$1"); if (diff !== "") { name = decodeDiff(name, diff); } return { name, qualifier }; } exports.decodeAttrName = decodeAttrName; /** * Transforms an attribute name from its unencoded form in the original XML data * (before transformation for use with wed) to its encoded name. * * The first thing this algorithm does is compute a difference between the * original XML name and how HTML will record it. The issue here is that XML * allows more characters in a name than what HTML allows and doing * ``setAttribute(name, value)`` will silently convert ``name`` to something * HTML likes. The issue most frequently encountered is that uppercase letters * are encoded as lowercase. This is especially vexing seeing as XML allows the * attribute names ``x`` and ``X`` to exist as different attributes, whereas * HTML does not. For HTML ``x`` and ``X`` are the same attribute. This function * records any differences between the original name and the way HTML records it * with a diff string that is appended to the final name after a dash. If * nothing appears after the final dash, then the HTML name and the XML name are * the same. * * A sequence of three dashes or more is converted by adding another dash. (So * sequences of single dash, or a pair of dashes remain unchanged. But all * sequences of 3 dashes or more gets an additional dash.) * * A colon (``:``) is converted to three dashes ``---``. * * After transformation above the name is prepended with ``data-wed-`` and it is * appended with the diff described above. * * Examples: * * - ``foo:bar`` becomes ``data-wed-foo---bar-``. Note how the diff is * empty, because ``foo:bar`` can be represented as-is in HTML. * * - ``MOO:aBc---def`` becomes ``data-wed-moo---abc----def-u3g2u1``. Note the * diff suffix, which allows restoring the orignal case. * * When ``qualifier`` is used, the qualifier is added just after ``data-wed-`` * and is prepended and appended with a dash. So ``foo:bar`` with the qualifier * ``ns`` would become ``data-wed--ns-foo---bar-``. The addition of a dash in * front of the qualifier makes it impossible to confuse an encoding that has a * qualifier from one that does not, as XML attribute names are not allowed to * start with a dash. * * @param name The unencoded name (i.e. the attribute name as it is in XML). * * @param qualifier An optional qualifier. * * @returns The encoded name. */ function encodeAttrName(name, qualifier) { const el = document.createElement("div"); // We havve to add the "data-" prefix to guard against some problems. IE11, // for instance, will choke if we set an attribute with the name "style". It // simply does not generally allow ``setAttribute("style", ...)``. Adding the // prefix, works around the problem. And we know "data-" will not be mangled, // so we can just strip it afterwards. el.setAttribute(`data-${name}`, ""); // Slice it to remove the "data-" prefix. const attrName = el.attributes[0].name.slice(5); const sanitized = attrName.replace(/--(-+)/g, "---$1").replace(/:/, "---"); qualifier = qualifier === undefined ? "" : `-${qualifier}-`; return `data-wed-${qualifier}${sanitized}-${encodeDiff(name, attrName)}`; } exports.encodeAttrName = encodeAttrName; /** * Determines whether a ``data-wed-`` attribute corresponds to an XML attribute. */ function isXMLAttrName(name) { return /^data-wed-(?!-)/.test(name); } exports.isXMLAttrName = isXMLAttrName; /** * Gets all the attributes of the node that were "original" attributes in the * XML document being edited, by opposition to those attributes that exist only * for HTML rendering. * * @param node The node to process. * * @returns An object whose keys are attribute names and values are attribute * values. */ function getOriginalAttributes(node) { const original = Object.create(null); const attributes = node.attributes; for (let i = 0; i < attributes.length; ++i) { const attr = attributes[i]; const localName = attr.localName; if (isXMLAttrName(localName)) { original[decodeAttrName(localName).name] = attr.value; } } return original; } exports.getOriginalAttributes = getOriginalAttributes; let nextID = 0; /** * Generates a new generic element id. This id is guaranteed to be unique for * the current run of wed. The ids generated by this function are meant to be * eventually replaced by something more permanent. * * @returns An element id. */ function newGenericID() { return `WED-ID-${++nextID}`; } exports.newGenericID = newGenericID; /** * @param ev A DOM event. * * @returns ``true`` if Control, Alt or Meta were held when the event was * created. Otherwise, ``false``. */ function anySpecialKeyHeld(ev) { const anyEv = ev; return anyEv.altKey || anyEv.ctrlKey || anyEv.metaKey; } exports.anySpecialKeyHeld = anySpecialKeyHeld; /** * **This function is meant to be used in debugging.** It creates a * ``selenium_log`` object on ``window`` which is an array that contains the * series of ``obj`` passed to this function. Remember that ultimately * ``selenium_log`` is going to be serialized by Selenium. So go easy on what * you put in there and be aware that Selenium may have bugs that prevent * serialization of certain objects. * * @param args Objects to log. */ /* tslint:disable:no-any no-unsafe-any */ function seleniumLog(...args) { const w = window; if (w.selenium_log === undefined) { w.selenium_log = []; } w.selenium_log.push.apply(w.selenium_log, args); } exports.seleniumLog = seleniumLog; function _exceptionStackTrace(err) { try { throw err; } catch (e) { return e.stack; } } /* tslint:enable */ /** * **This function is meant to be used in debugging.** Gets a stack trace. This * is only as cross-platform as needed for the platforms we support. * * Support for IE 9 is missing because it was designed by baboons. */ function stackTrace() { const err = new Error(); if (err.stack != null) { return err.stack; } // If the stack is not filled already (true of IE 10, 11) then raise an // exception to fill it. return _exceptionStackTrace(err); } exports.stackTrace = stackTrace; /** * Convert a "pattern object" to a string that can be shown to the user. This * function is meant to be used for "complex" name patterns that we may get from * salve. Note that a "pattern object" is the result of calling ``toObject()`` * on the pattern. The goal of this function is to convert the pattern object to * a string that would be interpretable by the end user. * * An explanation about how this handles namespaces and wildcard patterns is in * order. In a Relax NG schema the name pattern ``*`` in the compact notation is * equivalent to ``<anyName/>`` in the expanded notation. And ``foo:*`` is * equivalent to ``<nsName ns="uri_of_foo">`` where ``uri_of_foo`` is the URI * that has been associated with ``foo`` in the compact schema. It would be nice * if the function here could reuse this notation, but we cannot. Consider the * case where an Relax NG schema in the compact notation wants to declare a name * pattern which means "any name in the default namespace". In XML we express a * name in the default namespace currently in effect by simply not prefixing it * with a namespace name: whereas ``foo:bar`` is the ``bar`` element in the * ``foo`` namespace, ``bar`` is the ``bar`` element in the default * namespace. The pattern "any element in namespace foo" is represented with * ``foo:*``, however we cannot use ``*`` to mean "any element in the default * namespace", because ``*`` means "any name in any namespace whatsoever". The * compact notation forces the author of the schema to use a prefix for the * default namespace. And because of this, ``*`` means unambiguously "any * element in any namespace". * * So the ``*`` in the Relax NG schema becomes ``*:*`` here. "Any element in the * default namespace" is represented by ``*``. Thus ``foo:*`` and ``*`` can * stand in the same relation to one another as ``foo:bar`` and ``bar``. * * @param obj The "pattern object" to convert. * @param resolver The resolver to use to convert URIs to prefixes. * @returns The string representing the pattern. */ /* tslint:disable:no-any no-unsafe-any */ function convertPatternObj(obj, resolver) { // NameChoice if (obj.a != null && obj.b != null) { return `(${convertPatternObj(obj.a, resolver)}) or \ (${convertPatternObj(obj.b, resolver)})`; } let ret; // AnyName if (obj.pattern === "AnyName") { ret = "*:*"; } else { // Name and NsName if (obj.ns === undefined) { throw new Error("unexpected undefined obj.ns"); } if (obj.name !== undefined) { ret = resolver.unresolveName(obj.ns, obj.name); // Cannot unresolve, use the expanded name. if (ret === undefined) { ret = `{${obj.ns}}${obj.name}`; } } else { const ns = resolver.prefixFromURI(obj.ns); // If ns is undefined, we cannot resolve the URI, so we // display the expanded name. if (ns === undefined) { ret = `{${obj.ns}}`; } else { // An empty ns happens if the URI refers to the default // namespace. ret = (ns !== "") ? (`${ns}:`) : ns; } ret += "*"; } } if (obj.except != null) { ret += ` except (${convertPatternObj(obj.except, resolver)})`; } return ret; } exports.convertPatternObj = convertPatternObj; /* tslint:enable */ function readFile(file) { const reader = new FileReader(); return new Promise((resolve, reject) => { reader.onload = () => { resolve(reader.result); }; reader.onerror = reject; reader.readAsText(file); }); } exports.readFile = readFile; /** * This is required to work around a problem when extending built-in classes * like ``Error``. Some of the constructors for these classes return a value * from the constructor, which is then picked up by the constructors generated * by TypeScript (same with ES6 code transpiled through Babel), and this messes * up the inheritance chain. * * See https://github.com/Microsoft/TypeScript/issues/12123. */ // tslint:disable:no-any function fixPrototype(obj, parent) { const oldProto = Object.getPrototypeOf !== undefined ? Object.getPrototypeOf(obj) : obj.__proto__; if (oldProto !== parent) { if (Object.setPrototypeOf !== undefined) { Object.setPrototypeOf(obj, parent.prototype); } else { obj.__proto__ = parent.prototype; } } } exports.fixPrototype = fixPrototype; function suppressUnhandledRejections(p) { const pAsAny = p; if (pAsAny.suppressUnhandledRejections) { pAsAny.suppressUnhandledRejections(); } return p; } exports.suppressUnhandledRejections = suppressUnhandledRejections; }); // tslint:enable:no-any // LocalWords: Mangalam MPL Dubeau util CSS wed's unencoded URIs localName ns // LocalWords: escapeCSSClass xmlns prepended nextID NG NameChoice AnyName // LocalWords: convertPatternObj NsName //# sourceMappingURL=util.js.map