blob: b177e14caf7b23e76530e4b582db97e45ee7d521 [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// Astral code point as per https://mathiasbynens.be/notes/javascript-unicode
const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
export interface NormalizedRange {
endContainer: Node;
endOffset: number;
startContainer: Node;
startOffset: number;
}
/**
* Remap DOM range to whole lines of a diff if necessary. If the start or
* end containers are DOM elements that are singular pieces of syntax
* highlighting, the containers are remapped to the .contentText divs that
* contain the entire line of code.
*
* @param range - the standard DOM selector range.
* @return A modified version of the range that correctly accounts
* for syntax highlighting.
*/
export function normalize(range: Range): NormalizedRange {
const startContainer = getContentTextParent(range.startContainer);
const startOffset =
range.startOffset + getTextOffset(startContainer, range.startContainer);
const endContainer = getContentTextParent(range.endContainer);
const endOffset =
range.endOffset + getTextOffset(endContainer, range.endContainer);
return {
startContainer,
startOffset,
endContainer,
endOffset,
};
}
function getContentTextParent(target: Node): Node {
if (!target.parentElement) return target;
let element: Element | null;
if (target instanceof Element) {
element = target;
} else {
element = target.parentElement;
}
while (element && !element.classList.contains('contentText')) {
if (element.parentElement === null) {
return target;
}
element = element.parentElement;
}
return element ? element : target;
}
/**
* Gets the character offset of the child within the parent.
* Performs a synchronous in-order traversal from top to bottom of the node
* element, counting the length of the syntax until child is found.
*
* @param node The root DOM element to be searched through.
* @param child The child element being searched for.
*/
// TODO(TS): Only export for test.
export function getTextOffset(node: Node | null, child: Node): number {
let count = 0;
let stack = [node];
while (stack.length) {
const n = stack.pop();
if (n === child) {
break;
}
if (n?.childNodes && n.childNodes.length !== 0) {
const arr = [];
for (const childNode of n.childNodes) {
arr.push(childNode);
}
arr.reverse();
stack = stack.concat(arr);
} else {
count += getLength(n);
}
}
return count;
}
/**
* The DOM API textContent.length calculation is broken when the text
* contains Unicode. See https://mathiasbynens.be/notes/javascript-unicode .
*
* @param node A text node.
* @return The length of the text.
*/
function getLength(node?: Node | null) {
return node && node.textContent && node.nodeType !== Node.COMMENT_NODE
? node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length
: 0;
}