93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
|
// Copyright: Ankitects Pty Ltd and contributors
|
||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||
|
|
||
|
import { nodeIsElement } from "../../lib/dom";
|
||
|
import type { ElementMatcher } from "./matcher";
|
||
|
import { MatchResult } from "./matcher";
|
||
|
import { splitPartiallySelectedTextNodes } from "./text-node";
|
||
|
|
||
|
function textOrMatches(node: Node, matcher: ElementMatcher): boolean {
|
||
|
return !nodeIsElement(node) || matcher(node as Element) === MatchResult.MATCH;
|
||
|
}
|
||
|
|
||
|
function findBelow(element: Element, matcher: ElementMatcher): Node | null {
|
||
|
while (element.hasChildNodes()) {
|
||
|
const node = element.childNodes[element.childNodes.length - 1];
|
||
|
|
||
|
if (textOrMatches(node, matcher)) {
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
element = node as Element;
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
function findAbove(element: Element, matcher: ElementMatcher): Node | null {
|
||
|
if (element.parentNode) {
|
||
|
const index = Array.prototype.indexOf.call(element.parentNode, element);
|
||
|
|
||
|
if (index > 0) {
|
||
|
const before = element.parentNode.childNodes[index - 1];
|
||
|
|
||
|
if (textOrMatches(before, matcher)) {
|
||
|
return before;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
function findFittingNode(node: Node, matcher: ElementMatcher): Node {
|
||
|
if (textOrMatches(node, matcher)) {
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
return (
|
||
|
findBelow(node as Element, matcher) ??
|
||
|
findAbove(node as Element, matcher) ??
|
||
|
(console.log("anki: findFittingNode returns invalid node"), node)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function negate(matcher: ElementMatcher): ElementMatcher {
|
||
|
return (element: Element) => {
|
||
|
const matchResult = matcher(element);
|
||
|
|
||
|
switch (matchResult) {
|
||
|
case MatchResult.NO_MATCH:
|
||
|
return MatchResult.MATCH;
|
||
|
case MatchResult.MATCH:
|
||
|
return MatchResult.NO_MATCH;
|
||
|
default:
|
||
|
return matchResult;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
interface RangeAnchors {
|
||
|
start: Node;
|
||
|
end: Node;
|
||
|
}
|
||
|
|
||
|
export function getRangeAnchors(range: Range, matcher: ElementMatcher): RangeAnchors {
|
||
|
const { start, end } = splitPartiallySelectedTextNodes(range);
|
||
|
|
||
|
return {
|
||
|
start:
|
||
|
start ??
|
||
|
findFittingNode(
|
||
|
range.startContainer.childNodes[range.startOffset],
|
||
|
negate(matcher),
|
||
|
),
|
||
|
end:
|
||
|
end ??
|
||
|
findFittingNode(
|
||
|
range.endContainer.childNodes[range.endOffset - 1],
|
||
|
negate(matcher),
|
||
|
),
|
||
|
};
|
||
|
}
|