anki/ts/domlib/surround/unsurround.ts
Henrik Giesel 30bbbaf00b
Use eslint for sorting our imports (#1637)
* Make eslint sort our imports

* fix missing deps in eslint rule (dae)

Caught on Linux due to the stricter sandboxing

* Remove exports-last eslint rule (for now?)

* Adjust browserslist settings

- We use ResizeObserver which is not supported in browsers like KaiOS,
  Baidu or Android UC

* Raise minimum iOS version 13.4

- It's the first version that supports ResizeObserver

* Apply new eslint rules to sort imports
2022-02-04 18:36:34 +10:00

227 lines
6.6 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { findFarthest } from "./find-above";
import { findWithinNode, findWithinRange } from "./find-within";
import type { ElementClearer, ElementMatcher, FoundMatch } from "./matcher";
import { MatchResult, matchTagName } from "./matcher";
import type { NodesResult, SurroundNoSplittingResult } from "./no-splitting";
import { surround } from "./no-splitting";
import { getRangeAnchors } from "./range-anchors";
function findBetween(
range: Range,
matcher: ElementMatcher,
aboveStart?: Element | undefined,
aboveEnd?: Element | undefined,
): FoundMatch[] {
const betweenRange = range.cloneRange();
if (aboveStart) {
betweenRange.setStartAfter(aboveStart);
}
if (aboveEnd) {
betweenRange.setEndBefore(aboveEnd);
}
return findWithinRange(betweenRange, matcher);
}
function findAndClearWithin(
match: FoundMatch,
matcher: ElementMatcher,
clearer: ElementClearer,
condition: (node: Node) => boolean = () => true,
): Element[] {
const toRemove: Element[] = [];
for (const { matchType, element } of findWithinNode(match.element, matcher)) {
if (matchType === MatchResult.MATCH) {
if (condition(element)) {
toRemove.push(element);
}
} /* matchType === MatchResult.KEEP */ else {
// order is very important here as `clearer` is idempotent!
if (condition(element) && clearer(element)) {
toRemove.push(element);
}
}
}
if (condition(match.element)) {
switch (match.matchType) {
case MatchResult.MATCH:
toRemove.push(match.element);
break;
case MatchResult.KEEP:
if (clearer(match.element)) {
toRemove.push(match.element);
}
break;
}
}
return toRemove;
}
function prohibitOverlapse(withNode: Node): (node: Node) => boolean {
/* otherwise, they will be added to nodesToRemove twice
* and will also be cleared twice */
return (node: Node) => !node.contains(withNode) && !withNode.contains(node);
}
interface FindNodesToRemoveResult {
nodesToRemove: Element[];
beforeRange: Range;
afterRange: Range;
}
/**
* @returns beforeRange: will start at the farthest any of the nodes to remove will
* extend in start direction till the start of the original range
* @return afterRange: will start at the end of the original range and will extend as
* far as any of the nodes to remove will extend in end direction
*/
function findNodesToRemove(
range: Range,
base: Element,
matcher: ElementMatcher,
clearer: ElementClearer,
): FindNodesToRemoveResult {
const nodesToRemove: Element[] = [];
const aboveStart = findFarthest(range.startContainer, base, matcher);
const aboveEnd = findFarthest(range.endContainer, base, matcher);
const between = findBetween(range, matcher, aboveStart?.element, aboveEnd?.element);
const beforeRange = new Range();
beforeRange.setEnd(range.startContainer, range.startOffset);
beforeRange.collapse(false);
if (aboveStart) {
beforeRange.setStartBefore(aboveStart.element);
const matches = findAndClearWithin(
aboveStart,
matcher,
clearer,
aboveEnd ? prohibitOverlapse(aboveEnd.element) : () => true,
);
nodesToRemove.push(...matches);
}
nodesToRemove.push(...between.map((match) => match.element));
const afterRange = new Range();
afterRange.setStart(range.endContainer, range.endOffset);
afterRange.collapse(true);
if (aboveEnd) {
afterRange.setEndAfter(aboveEnd.element);
const matches = findAndClearWithin(aboveEnd, matcher, clearer);
nodesToRemove.push(...matches);
}
return {
nodesToRemove,
beforeRange,
afterRange,
};
}
function resurroundAdjacent(
beforeRange: Range,
afterRange: Range,
surroundNode: Element,
base: Element,
matcher: ElementMatcher,
clearer: ElementClearer,
): NodesResult {
const addedNodes: Node[] = [];
const removedNodes: Node[] = [];
if (beforeRange.toString().length > 0) {
const { addedNodes: added, removedNodes: removed } = surround(
beforeRange,
surroundNode,
base,
matcher,
clearer,
);
addedNodes.push(...added);
removedNodes.push(...removed);
}
if (afterRange.toString().length > 0) {
const { addedNodes: added, removedNodes: removed } = surround(
afterRange,
surroundNode,
base,
matcher,
clearer,
);
addedNodes.push(...added);
removedNodes.push(...removed);
}
return { addedNodes, removedNodes };
}
/**
* Avoids splitting existing elements in the surrounded area
* might create multiple of the surrounding element and remove elements specified by matcher
* can be used for inline elements e.g. <b>, or <strong>
* @param range: The range to surround
* @param surroundNode: This node will be shallowly cloned for surrounding
* @param base: Surrounding will not ascent beyond this point; base.contains(range.commonAncestorContainer) should be true
* @param matcher: Used to detect elements will are similar to the surroundNode, and are included in normalization
* @param clearer: Used to clear elements which have unwanted properties
**/
export function unsurround(
range: Range,
surroundNode: Element,
base: Element,
matcher: ElementMatcher = matchTagName(surroundNode.tagName),
clearer: ElementClearer = () => false,
): SurroundNoSplittingResult {
const { start, end } = getRangeAnchors(range, matcher);
const { nodesToRemove, beforeRange, afterRange } = findNodesToRemove(
range,
base,
matcher,
clearer,
);
/**
* We cannot remove the nodes immediately, because they would make the ranges collapse
*/
const { addedNodes, removedNodes } = resurroundAdjacent(
beforeRange,
afterRange,
surroundNode,
base,
matcher,
clearer,
);
for (const node of nodesToRemove) {
if (node.isConnected) {
removedNodes.push(node);
node.replaceWith(...node.childNodes);
}
}
const surroundedRange = new Range();
surroundedRange.setStartBefore(start);
surroundedRange.setEndAfter(end);
base.normalize();
return {
addedNodes,
removedNodes,
surroundedRange,
};
}