Tommy Martino | 7d7928e | 2020-07-28 15:54:34 -0400 | [diff] [blame] | 1 | /** |
| 2 | * Copyright 2020 Google LLC |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * https://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | const FRAGMENT_DIRECTIVES = ['text']; |
| 18 | |
| 19 | export const getFragmentDirectives = (hash) => { |
| 20 | const fragmentDirectivesString = hash.replace(/#.*?:~:(.*?)/, '$1'); |
| 21 | if (!fragmentDirectivesString) { |
| 22 | return; |
| 23 | } |
| 24 | const fragmentDirectivesParams = new URLSearchParams( |
| 25 | fragmentDirectivesString, |
| 26 | ); |
| 27 | const fragmentDirectives = {}; |
| 28 | FRAGMENT_DIRECTIVES.forEach((fragmentDirectiveType) => { |
| 29 | if (fragmentDirectivesParams.has(fragmentDirectiveType)) { |
| 30 | fragmentDirectives[ |
| 31 | fragmentDirectiveType |
| 32 | ] = fragmentDirectivesParams.getAll(fragmentDirectiveType); |
| 33 | } |
| 34 | }); |
| 35 | return fragmentDirectives; |
| 36 | }; |
| 37 | |
| 38 | export const parseFragmentDirectives = (fragmentDirectives) => { |
| 39 | const parsedFragmentDirectives = {}; |
| 40 | for (const [ |
| 41 | fragmentDirectiveType, |
| 42 | fragmentDirectivesOfType, |
| 43 | ] of Object.entries(fragmentDirectives)) { |
| 44 | if (fragmentDirectiveType === 'text') { |
| 45 | parsedFragmentDirectives[ |
| 46 | fragmentDirectiveType |
| 47 | ] = fragmentDirectivesOfType.map((fragmentDirectiveOfType) => { |
| 48 | return parseTextFragmentDirective(fragmentDirectiveOfType); |
| 49 | }); |
| 50 | } |
| 51 | } |
| 52 | return parsedFragmentDirectives; |
| 53 | }; |
| 54 | |
| 55 | const parseTextFragmentDirective = (textFragment) => { |
| 56 | const TEXT_FRAGMENT = /^(?:(.+?)-,)?(?:(.+?))(?:,(.+?))?(?:,-(.+?))?$/; |
| 57 | return { |
| 58 | prefix: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$1')), |
| 59 | textStart: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$2')), |
| 60 | textEnd: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$3')), |
| 61 | suffix: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$4')), |
| 62 | }; |
| 63 | }; |
| 64 | |
| 65 | export const processFragmentDirectives = (parsedFragmentDirectives) => { |
| 66 | const processedFragmentDirectives = {}; |
| 67 | for (const [ |
| 68 | fragmentDirectiveType, |
| 69 | fragmentDirectivesOfType, |
| 70 | ] of Object.entries(parsedFragmentDirectives)) { |
| 71 | if (fragmentDirectiveType === 'text') { |
| 72 | processedFragmentDirectives[ |
| 73 | fragmentDirectiveType |
| 74 | ] = fragmentDirectivesOfType.map((fragmentDirectiveOfType) => { |
| 75 | return processTextFragmentDirective(fragmentDirectiveOfType); |
| 76 | }); |
| 77 | } |
| 78 | } |
| 79 | return processedFragmentDirectives; |
| 80 | }; |
| 81 | |
| 82 | const escapeRegExp = (s) => { |
| 83 | return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); |
| 84 | }; |
| 85 | |
| 86 | const processTextFragmentDirective = (textFragment) => { |
| 87 | const prefixNodes = findText(textFragment.prefix); |
| 88 | const textStartNodes = findText(textFragment.textStart); |
| 89 | const textEndNodes = findText(textFragment.textEnd); |
| 90 | const suffixNodes = findText(textFragment.suffix); |
| 91 | const scrollBehavior = { |
| 92 | behavior: 'smooth', |
| 93 | block: 'nearest', |
| 94 | inline: 'nearest', |
| 95 | }; |
| 96 | if ( |
| 97 | !prefixNodes.length && |
| 98 | !suffixNodes.length && |
| 99 | textStartNodes.length === 1 && |
| 100 | textStartNodes[0].parentNode |
| 101 | ) { |
| 102 | // Only `textStart` |
| 103 | if (!textEndNodes.length) { |
| 104 | const textStartNode = textStartNodes[0].parentNode; |
| 105 | const adjacentHTML = textStartNodes[0].textContent.replace( |
| 106 | new RegExp( |
| 107 | `(^.*?)(${escapeRegExp(textFragment.textStart)})(.*?$)`, |
| 108 | 'm', |
| 109 | ), |
| 110 | '$1<mark>$2</mark>$3', |
| 111 | ); |
| 112 | textStartNode.textContent = ''; |
| 113 | textStartNode.insertAdjacentHTML('afterbegin', adjacentHTML); |
| 114 | textStartNode.scrollIntoView(scrollBehavior); |
| 115 | // Only `textStart` and `textEnd` |
| 116 | } else if (textEndNodes.length === 1 && textEndNodes[0].parentNode) { |
| 117 | // If `textStart` and `textEnd` are in the same node |
| 118 | if (textEndNodes[0].parentNode === textStartNodes[0].parentNode) { |
| 119 | const textStartNode = textStartNodes[0].parentNode; |
| 120 | const adjacentHTML = textStartNodes[0].textContent.replace( |
| 121 | new RegExp( |
| 122 | `(^.*?)(${escapeRegExp(textFragment.textStart)})(.*?)(${ |
| 123 | textFragment.textEnd |
| 124 | })(.*?$)`, |
| 125 | 'm', |
| 126 | ), |
| 127 | '$1<mark>$2$3$4</mark>$5', |
| 128 | ); |
| 129 | textStartNode.textContent = ''; |
| 130 | textStartNode.insertAdjacentHTML('afterbegin', adjacentHTML); |
| 131 | textStartNode.scrollIntoView(scrollBehavior); |
| 132 | // If `textStart` and `textEnd` are in different nodes |
| 133 | } else { |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | if (prefixNodes.length) { |
| 138 | } |
| 139 | }; |
| 140 | |
| 141 | const findText = (text) => { |
| 142 | if (!text) { |
| 143 | return []; |
| 144 | } |
| 145 | const body = document.body; |
| 146 | const treeWalker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, { |
| 147 | acceptNode: (node) => { |
| 148 | if (node.textContent.includes(text)) { |
| 149 | return NodeFilter.FILTER_ACCEPT; |
| 150 | } |
| 151 | }, |
| 152 | }); |
| 153 | |
| 154 | const nodeList = []; |
| 155 | let currentNode = treeWalker.currentNode; |
| 156 | while (currentNode) { |
| 157 | nodeList.push(currentNode); |
| 158 | currentNode = treeWalker.nextNode(); |
| 159 | } |
| 160 | return nodeList.slice(1); |
| 161 | }; |