wed
Version:
Wed is a schema-aware editor for XML documents.
365 lines • 15.3 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", "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