blob: 3d335d42c25fc6583eb389d38ae7526423970b40 [file] [log] [blame]
Tommy Martino7d7928e2020-07-28 15:54:34 -04001/**
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
17const FRAGMENT_DIRECTIVES = ['text'];
18
19export 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
38export 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
55const 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
65export 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
82const escapeRegExp = (s) => {
83 return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
84};
85
86const 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
141const 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};