| /** |
| * @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; |
| } |