/** * @license * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // 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.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length : 0; }