fix(flexsearch): enhance match highlighting and safe DOM manipulation (#903)

- Replaced the highlightMatches function with appendHighlightedText to improve match highlighting using safe text nodes.
- Updated DOM manipulation for search results to create elements directly instead of using innerHTML, enhancing security and performance.
- Ensured consistent handling of empty text and query cases in the new highlighting function.
This commit is contained in:
Xin
2026-01-18 19:59:09 +00:00
committed by GitHub
parent 2b00f92a42
commit a9ef7ad57d

View File

@@ -392,18 +392,36 @@ document.addEventListener("DOMContentLoaded", function () {
return; return;
} }
// Highlight the query in the result text. // Append text with highlighted matches using safe text nodes.
function highlightMatches(text, query) { function appendHighlightedText(container, text, query) {
const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&'); if (!text) return;
const regex = new RegExp(escapedQuery, 'gi'); if (!query) {
return text.replace(regex, (match) => `<span class="hextra-search-match">${match}</span>`); container.textContent = text;
return;
} }
// Create a DOM element from the HTML string. const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
function createElement(str) { if (!escapedQuery) {
const div = document.createElement('div'); container.textContent = text;
div.innerHTML = str.trim(); return;
return div.firstChild; }
const regex = new RegExp(escapedQuery, 'gi');
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
container.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
}
const span = document.createElement('span');
span.className = 'hextra-search-match';
span.textContent = match[0];
container.appendChild(span);
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
container.appendChild(document.createTextNode(text.slice(lastIndex)));
}
} }
function handleMouseMove(e) { function handleMouseMove(e) {
@@ -421,20 +439,35 @@ document.addEventListener("DOMContentLoaded", function () {
for (let i = 0; i < results.length; i++) { for (let i = 0; i < results.length; i++) {
const result = results[i]; const result = results[i];
if (result.prefix) { if (result.prefix) {
fragment.appendChild(createElement(` const prefix = document.createElement('div');
<div class="hextra-search-prefix">${result.prefix}</div>`)); prefix.className = 'hextra-search-prefix';
prefix.textContent = result.prefix;
fragment.appendChild(prefix);
} }
let li = createElement(` const li = document.createElement('li');
<li> const link = document.createElement('a');
<a data-index="${i}" href="${result.route}" class=${i === 0 ? "hextra-search-active" : ""}> link.dataset.index = i;
<div class="hextra-search-title">`+ highlightMatches(result.children.title, query) + `</div>` + link.href = result.route;
(result.children.content ? if (i === 0) {
`<div class="hextra-search-excerpt">` + highlightMatches(result.children.content, query) + `</div>` : '') + ` link.classList.add('hextra-search-active');
</a> }
</li>`);
const title = document.createElement('div');
title.className = 'hextra-search-title';
appendHighlightedText(title, result.children.title, query);
link.appendChild(title);
if (result.children.content) {
const excerpt = document.createElement('div');
excerpt.className = 'hextra-search-excerpt';
appendHighlightedText(excerpt, result.children.content, query);
link.appendChild(excerpt);
}
li.appendChild(link);
li.addEventListener('mousemove', handleMouseMove); li.addEventListener('mousemove', handleMouseMove);
li.addEventListener('keydown', handleKeyDown); li.addEventListener('keydown', handleKeyDown);
li.querySelector('a').addEventListener('click', finishSearch); link.addEventListener('click', finishSearch);
fragment.appendChild(li); fragment.appendChild(li);
} }
resultsElement.appendChild(fragment); resultsElement.appendChild(fragment);