UNPKG

wed

Version:

Wed is a schema-aware editor for XML documents.

365 lines 15.3 kB
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", "rangy", "./dloc", "./domtypeguards", "./domutil"], function (require, exports, rangy, dloc_1, domtypeguards_1, domutil_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); rangy = __importStar(rangy); /** The direction of searches. */ var Direction; (function (Direction) { Direction[Direction["FORWARD"] = 0] = "FORWARD"; Direction[Direction["BACKWARDS"] = 1] = "BACKWARDS"; })(Direction = exports.Direction || (exports.Direction = {})); /** The context for searches. */ var Context; (function (Context) { /** Everywhere in a document, including non-editable graphical elements. */ Context[Context["EVERYWHERE"] = 0] = "EVERYWHERE"; /** Only element text. */ Context[Context["TEXT"] = 1] = "TEXT"; /** Only attribute values. */ Context[Context["ATTRIBUTE_VALUES"] = 2] = "ATTRIBUTE_VALUES"; })(Context = exports.Context || (exports.Context = {})); function unknownDirection(d) { throw new Error(`unknown direction: ${d}`); } function directionToRangyDirection(direction) { // There does not seem to be a way currently to declare this map in a way // that will enforce that all directions have a value. :-/ // tslint:disable-next-line:no-object-literal-type-assertion const ret = { [Direction.FORWARD]: "forward", [Direction.BACKWARDS]: "backward", }[direction]; if (ret === undefined) { // We have to cast to never since we're not using the switch exhaustion. return unknownDirection(direction); } return ret; } function nodeInScope(doc, node, scope) { const vrange = doc.createRange(); vrange.selectNode(node); // The range that encompasses the node, must be completely within scope. return (vrange.compareBoundaryPoints(Range.START_TO_START, scope) >= 0) && (vrange.compareBoundaryPoints(Range.END_TO_END, scope) <= 0); } /** * This is a utility class that holds a position among a list of elements * (representing attributes, in our usage). */ class AttributeValueCursor { /** * @param values The values to iterate over. * * @param direction The direction to iterate over. */ constructor(values, direction) { this.values = values; this.direction = direction; this.resetToStart(); } /** * @param value The index to reset this iterator to. */ reset(value) { this.current = value; } /** * @param value Reset to the start of [[values]]. This will be position 0 for * an iterator moving forward. Or the end of [[values]] for an iterator moving * backwards. */ resetToStart() { switch (this.direction) { case Direction.FORWARD: this.current = 0; break; case Direction.BACKWARDS: this.current = this.values.length - 1; break; default: return unknownDirection(this.direction); } } /** * @returns ``true`` if we have not reached the end of the array. */ get hasNext() { switch (this.direction) { case Direction.FORWARD: return this.current < this.values.length; case Direction.BACKWARDS: return this.current >= 0; default: return unknownDirection(this.direction); } } /** * This is the next element in iteration order. Moves the iterator in the * direction of travel. */ get next() { const ret = this.values[this.current]; this.inc(); return ret; } /** * Moves the iterator in the direction of travel. */ inc() { switch (this.direction) { case Direction.FORWARD: this.current++; break; case Direction.BACKWARDS: this.current--; break; default: return unknownDirection(this.direction); } } } /** * This models a search on the GUI tree. Performing searches directly on the * data tree is theoretically possible but fraught with problems. For instance, * some data may not be visible to users and so the search in the data tree * would have to constantly refer to the GUI tree to determine whether a hit * should be shown. Additionally, the order of the data shown in the GUI tree * may differ from the order in the data tree. */ class Search { constructor(caretManager, guiRoot, start, scope) { this.caretManager = caretManager; this.guiRoot = guiRoot; this.start = start; this._pattern = ""; /** The direction in which the search moves. */ this.direction = Direction.FORWARD; /** The context for the search. */ this.context = Context.EVERYWHERE; this.root = dloc_1.getRoot(guiRoot); this.setScope(scope); const realScope = this.scope; if (realScope.start.compare(start) > 0 || realScope.end.compare(start) < 0) { throw new Error("the scope does not contain the start position"); } this.start = start; } set pattern(value) { this._pattern = value; } get pattern() { return this._pattern; } /** * Set the search scope. No result will be returned outside the scope. Setting * the scope to ``undefined`` means "search the whole document". */ setScope(range) { if (range === undefined) { this._scope = undefined; return; } if (!range.isValid()) { throw new Error("passed an invalid range"); } const { start, end } = range; if (start.root !== this.root.node) { throw new Error("the range does not use the search's root"); } // Since the start and end of a range must share the same root, we don't // have to test the end of the range. this._scope = start.compare(end) > 0 ? // Start is after end, flip them. new dloc_1.DLocRange(end, start) : // Regular order new dloc_1.DLocRange(start, end); } get scope() { if (this._scope === undefined) { this._scope = new dloc_1.DLocRange(dloc_1.DLoc.mustMakeDLoc(this.root, this.guiRoot, 0), dloc_1.DLoc.mustMakeDLoc(this.root, this.guiRoot, this.guiRoot.childNodes.length)); } return this._scope; } updateCurrent() { this._next(true); } next() { this._next(false); } _next(includeCurrent) { let ret = null; if (this.pattern !== "") { let rollPosition; let start; switch (this.direction) { case Direction.FORWARD: { start = this.getForwardSearchStart(includeCurrent); rollPosition = this.scope.start; break; } case Direction.BACKWARDS: { start = this.getBackwardSearchStart(includeCurrent); rollPosition = this.scope.end; break; } default: return unknownDirection(this.direction); } const hit = this.find(start, this.direction); if (hit !== null) { ret = new dloc_1.DLocRange(dloc_1.DLoc.mustMakeDLoc(this.root, hit.startContainer, hit.startOffset), dloc_1.DLoc.mustMakeDLoc(this.root, hit.endContainer, hit.endOffset)); } else { // If we did not get a hit, we roll over on the next search. this.start = rollPosition; } } this.current = ret; } find(start, direction) { if (this.context === Context.ATTRIBUTE_VALUES) { return this.findAttributeValue(start, direction); } return this.findText(start, directionToRangyDirection(direction)); } findText(start, direction) { // tslint:disable-next-line:no-any const config = rangy.config; const range = new rangy.WrappedRange(start.makeRange()); if (this.context === Context.TEXT) { config.customIsCollapsedNode = (node) => { return domtypeguards_1.isElement(node) && node.closest("._phantom") !== null; }; } let found = range.findText(this.pattern, { withinRange: this.scope.mustMakeDOMRange(), direction, }); // There is a bug in Rangy that makes it so that it may sometimes return // hits outside the scope. Test for it. if (found) { const hitStart = dloc_1.DLoc.mustMakeDLoc(this.guiRoot, range.startContainer, range.startOffset); if (!this.scope.contains(hitStart)) { found = false; } } config.customIsCollapsedNode = undefined; return found ? range.nativeRange : null; } findAttributeValue(start, direction) { // Implement our own logic instead of relying on rangy. We can just move // from attribute value to attribute value and checks the values. const guiRoot = this.guiRoot; const allValues = Array.from(guiRoot.getElementsByClassName("_attribute_value")); const caretManager = this.caretManager; const valueCursor = new AttributeValueCursor(allValues, direction); const attrValue = domutil_1.closestByClass(start.node, "_attribute_value", guiRoot); const doc = guiRoot.ownerDocument; const scope = this.scope.mustMakeDOMRange(); if (attrValue === null) { // We need to find the next attribute. let found; while (valueCursor.hasNext) { const value = valueCursor.next; if (nodeInScope(doc, value, scope) && // tslint:disable-next-line:no-bitwise ((value.compareDocumentPosition(start.node) & Node.DOCUMENT_POSITION_PRECEDING) !== 0)) { found = value; break; } } if (found === undefined) { return null; } start = start.make(found, 0); } else { const index = allValues.indexOf(attrValue); if (index === -1) { throw new Error("internal error: cannot find value in array!"); } valueCursor.reset(index); valueCursor.inc(); } let dataLoc = caretManager.toDataLocation(start); // tslint:disable-next-line:no-constant-condition while (true) { // Going into the data tree simplifies some of the work here. const node = dataLoc.node; let index; switch (direction) { case Direction.FORWARD: index = node.value.indexOf(this.pattern, dataLoc.offset); break; case Direction.BACKWARDS: // For a backward search, the hit is not allowed to cross the start // position. (This, by the way, is the same way Emacs operates.) const startOffset = dataLoc.offset - this.pattern.length; index = (startOffset < 0) ? -1 : node.value.lastIndexOf(this.pattern, startOffset); break; default: return unknownDirection(direction); } if (index !== -1) { const rangeStart = caretManager.mustFromDataLocation(dataLoc.makeWithOffset(index)); const rangeEnd = caretManager.mustFromDataLocation(dataLoc.makeWithOffset(index + this.pattern.length)); if (this.scope.contains(rangeStart) && this.scope.contains(rangeEnd)) { return new dloc_1.DLocRange(rangeStart, rangeEnd).mustMakeDOMRange(); } } // We did not find a good hit, so continue searching other values. let next = null; while (next === null && valueCursor.hasNext) { next = valueCursor.next; if (!nodeInScope(doc, next, scope)) { next = null; } } if (next === null) { return null; } let dataNext = caretManager.toDataLocation(next, 0); switch (direction) { case Direction.FORWARD: break; case Direction.BACKWARDS: dataNext = dataNext.makeWithOffset(dataNext.node.value.length); break; default: return unknownDirection(direction); } dataLoc = dataNext; } } getForwardSearchStart(includeCurrent) { if (this.current != null) { return includeCurrent ? this.current.start : this.current.end; } return this.start; } getBackwardSearchStart(includeCurrent) { let ret; if (this.current != null) { ret = includeCurrent ? this.prevEnd : this.current.start; } if (ret === undefined) { ret = this.start; } this.prevEnd = ret; return ret; } } exports.Search = Search; }); //# sourceMappingURL=search.js.map