blob: 3d335d42c25fc6583eb389d38ae7526423970b40 [file] [log] [blame]
/**
* Copyright 2020 Google LLC
*
* 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
*
* https://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.
*/
const FRAGMENT_DIRECTIVES = ['text'];
export const getFragmentDirectives = (hash) => {
const fragmentDirectivesString = hash.replace(/#.*?:~:(.*?)/, '$1');
if (!fragmentDirectivesString) {
return;
}
const fragmentDirectivesParams = new URLSearchParams(
fragmentDirectivesString,
);
const fragmentDirectives = {};
FRAGMENT_DIRECTIVES.forEach((fragmentDirectiveType) => {
if (fragmentDirectivesParams.has(fragmentDirectiveType)) {
fragmentDirectives[
fragmentDirectiveType
] = fragmentDirectivesParams.getAll(fragmentDirectiveType);
}
});
return fragmentDirectives;
};
export const parseFragmentDirectives = (fragmentDirectives) => {
const parsedFragmentDirectives = {};
for (const [
fragmentDirectiveType,
fragmentDirectivesOfType,
] of Object.entries(fragmentDirectives)) {
if (fragmentDirectiveType === 'text') {
parsedFragmentDirectives[
fragmentDirectiveType
] = fragmentDirectivesOfType.map((fragmentDirectiveOfType) => {
return parseTextFragmentDirective(fragmentDirectiveOfType);
});
}
}
return parsedFragmentDirectives;
};
const parseTextFragmentDirective = (textFragment) => {
const TEXT_FRAGMENT = /^(?:(.+?)-,)?(?:(.+?))(?:,(.+?))?(?:,-(.+?))?$/;
return {
prefix: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$1')),
textStart: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$2')),
textEnd: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$3')),
suffix: decodeURIComponent(textFragment.replace(TEXT_FRAGMENT, '$4')),
};
};
export const processFragmentDirectives = (parsedFragmentDirectives) => {
const processedFragmentDirectives = {};
for (const [
fragmentDirectiveType,
fragmentDirectivesOfType,
] of Object.entries(parsedFragmentDirectives)) {
if (fragmentDirectiveType === 'text') {
processedFragmentDirectives[
fragmentDirectiveType
] = fragmentDirectivesOfType.map((fragmentDirectiveOfType) => {
return processTextFragmentDirective(fragmentDirectiveOfType);
});
}
}
return processedFragmentDirectives;
};
const escapeRegExp = (s) => {
return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
};
const processTextFragmentDirective = (textFragment) => {
const prefixNodes = findText(textFragment.prefix);
const textStartNodes = findText(textFragment.textStart);
const textEndNodes = findText(textFragment.textEnd);
const suffixNodes = findText(textFragment.suffix);
const scrollBehavior = {
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
};
if (
!prefixNodes.length &&
!suffixNodes.length &&
textStartNodes.length === 1 &&
textStartNodes[0].parentNode
) {
// Only `textStart`
if (!textEndNodes.length) {
const textStartNode = textStartNodes[0].parentNode;
const adjacentHTML = textStartNodes[0].textContent.replace(
new RegExp(
`(^.*?)(${escapeRegExp(textFragment.textStart)})(.*?$)`,
'm',
),
'$1<mark>$2</mark>$3',
);
textStartNode.textContent = '';
textStartNode.insertAdjacentHTML('afterbegin', adjacentHTML);
textStartNode.scrollIntoView(scrollBehavior);
// Only `textStart` and `textEnd`
} else if (textEndNodes.length === 1 && textEndNodes[0].parentNode) {
// If `textStart` and `textEnd` are in the same node
if (textEndNodes[0].parentNode === textStartNodes[0].parentNode) {
const textStartNode = textStartNodes[0].parentNode;
const adjacentHTML = textStartNodes[0].textContent.replace(
new RegExp(
`(^.*?)(${escapeRegExp(textFragment.textStart)})(.*?)(${
textFragment.textEnd
})(.*?$)`,
'm',
),
'$1<mark>$2$3$4</mark>$5',
);
textStartNode.textContent = '';
textStartNode.insertAdjacentHTML('afterbegin', adjacentHTML);
textStartNode.scrollIntoView(scrollBehavior);
// If `textStart` and `textEnd` are in different nodes
} else {
}
}
}
if (prefixNodes.length) {
}
};
const findText = (text) => {
if (!text) {
return [];
}
const body = document.body;
const treeWalker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
if (node.textContent.includes(text)) {
return NodeFilter.FILTER_ACCEPT;
}
},
});
const nodeList = [];
let currentNode = treeWalker.currentNode;
while (currentNode) {
nodeList.push(currentNode);
currentNode = treeWalker.nextNode();
}
return nodeList.slice(1);
};