mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: add working workers with builds
This commit is contained in:
@@ -1,88 +1,112 @@
|
||||
import type { FuseResultMatch, MatchIndices } from './types';
|
||||
import type { FuseResultMatch, MatchIndices } from "./core/types";
|
||||
|
||||
/**
|
||||
* Simple utility to remove HTML tags from a string.
|
||||
*/
|
||||
export function stripHtmlTags(html: string): string {
|
||||
if (!html) return '';
|
||||
return html.replace(/<[^>]*>/g, '').replace('\n', ' ');
|
||||
if (!html) return "";
|
||||
return html.replace(/<[^>]*>/g, "").replace("\n", " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes HTML tags from a string, but preserves <span class="highlight"> tags.
|
||||
*/
|
||||
export function stripHtmlButKeepHighlights(html: string): string {
|
||||
if (!html) return '';
|
||||
if (!html) return "";
|
||||
// Use a placeholder for highlight tags, strip others, then restore placeholders.
|
||||
const highlightOpenPlaceholder = '__HIGHLIGHT_OPEN__';
|
||||
const highlightClosePlaceholder = '__HIGHLIGHT_CLOSE__';
|
||||
const highlightOpenPlaceholder = "__HIGHLIGHT_OPEN__";
|
||||
const highlightClosePlaceholder = "__HIGHLIGHT_CLOSE__";
|
||||
|
||||
let processed = html.replace(/<span class="highlight">/g, highlightOpenPlaceholder);
|
||||
let processed = html.replace(
|
||||
/<span class="highlight">/g,
|
||||
highlightOpenPlaceholder,
|
||||
);
|
||||
processed = processed.replace(/<\/span>/g, (match, offset, fullString) => {
|
||||
// Only replace </span> if it likely corresponds to our highlight span
|
||||
// This is imperfect but helps avoid replacing unrelated spans.
|
||||
// Look backwards for the nearest opening placeholder.
|
||||
const lastPlaceholder = fullString.lastIndexOf(highlightOpenPlaceholder, offset);
|
||||
const lastPlaceholder = fullString.lastIndexOf(
|
||||
highlightOpenPlaceholder,
|
||||
offset,
|
||||
);
|
||||
if (lastPlaceholder !== -1) {
|
||||
// Check if there's another opening tag between the placeholder and the closing span
|
||||
const interveningContent = fullString.substring(lastPlaceholder + highlightOpenPlaceholder.length, offset);
|
||||
const interveningContent = fullString.substring(
|
||||
lastPlaceholder + highlightOpenPlaceholder.length,
|
||||
offset,
|
||||
);
|
||||
if (!/<span/i.test(interveningContent)) {
|
||||
return highlightClosePlaceholder;
|
||||
}
|
||||
}
|
||||
return match; // Keep the original </span> if unsure
|
||||
});
|
||||
|
||||
|
||||
// Strip all remaining HTML tags
|
||||
processed = processed.replace(/<[^>]*>/g, '');
|
||||
processed = processed.replace(/<[^>]*>/g, "");
|
||||
|
||||
// Restore the highlight tags
|
||||
processed = processed.replace(new RegExp(highlightOpenPlaceholder, 'g'), '<span class="highlight">');
|
||||
processed = processed.replace(new RegExp(highlightClosePlaceholder, 'g'), '</span>');
|
||||
processed = processed.replace(
|
||||
new RegExp(highlightOpenPlaceholder, "g"),
|
||||
'<span class="highlight">',
|
||||
);
|
||||
processed = processed.replace(
|
||||
new RegExp(highlightClosePlaceholder, "g"),
|
||||
"</span>",
|
||||
);
|
||||
|
||||
return processed;
|
||||
}
|
||||
|
||||
export function highlightMatch(
|
||||
text: string,
|
||||
term: string,
|
||||
matches?: readonly FuseResultMatch[]
|
||||
text: string,
|
||||
term: string,
|
||||
matches?: readonly FuseResultMatch[],
|
||||
): string {
|
||||
if (!term.trim() || !matches || matches.length === 0) return text;
|
||||
|
||||
|
||||
try {
|
||||
// Find matches for the text field or allContent that contains the text
|
||||
const fieldMatches = matches.find(match =>
|
||||
match.key === 'text' ||
|
||||
(match.key === 'allContent' && match.value?.includes(text))
|
||||
const fieldMatches = matches.find(
|
||||
(match) =>
|
||||
match.key === "text" ||
|
||||
(match.key === "allContent" && match.value?.includes(text)),
|
||||
);
|
||||
|
||||
if (!fieldMatches || !fieldMatches.indices || fieldMatches.indices.length === 0) {
|
||||
|
||||
if (
|
||||
!fieldMatches ||
|
||||
!fieldMatches.indices ||
|
||||
fieldMatches.indices.length === 0
|
||||
) {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
// Create a map of character positions to mark which ones need highlighting
|
||||
const highlightMap = new Array(text.length).fill(false);
|
||||
|
||||
|
||||
fieldMatches.indices.forEach((indices: MatchIndices) => {
|
||||
const start = indices[0];
|
||||
const end = indices[1];
|
||||
|
||||
if (fieldMatches.key === 'allContent') {
|
||||
|
||||
if (fieldMatches.key === "allContent") {
|
||||
// Find where our text appears in the allContent
|
||||
const allContent = fieldMatches.value;
|
||||
const textPos = allContent?.indexOf(text) ?? -1;
|
||||
|
||||
|
||||
// Only highlight if the match overlaps with our text
|
||||
if (textPos >= 0) {
|
||||
// Adjust start and end to be relative to our text field
|
||||
const relStart = start - textPos;
|
||||
const relEnd = end - textPos;
|
||||
|
||||
|
||||
// Only highlight if the match actually overlaps with our text field
|
||||
if (relEnd >= 0 && relStart < text.length) {
|
||||
// Mark the overlapping characters
|
||||
for (let i = Math.max(0, relStart); i <= Math.min(text.length - 1, relEnd); i++) {
|
||||
for (
|
||||
let i = Math.max(0, relStart);
|
||||
i <= Math.min(text.length - 1, relEnd);
|
||||
i++
|
||||
) {
|
||||
highlightMap[i] = true;
|
||||
}
|
||||
}
|
||||
@@ -96,106 +120,121 @@ export function highlightMatch(
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = '';
|
||||
|
||||
let result = "";
|
||||
let inHighlight = false;
|
||||
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (highlightMap[i] && !inHighlight) {
|
||||
result += '<span class="highlight">';
|
||||
inHighlight = true;
|
||||
} else if (!highlightMap[i] && inHighlight) {
|
||||
result += '</span>';
|
||||
result += "</span>";
|
||||
inHighlight = false;
|
||||
}
|
||||
|
||||
|
||||
result += text.charAt(i);
|
||||
}
|
||||
|
||||
|
||||
if (inHighlight) {
|
||||
result += '</span>';
|
||||
result += "</span>";
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('Error highlighting match:', e);
|
||||
console.error("Error highlighting match:", e);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to extract and highlight content snippet using Fuse matches
|
||||
export function highlightSnippet(
|
||||
content: string,
|
||||
term: string,
|
||||
matches?: readonly FuseResultMatch[]
|
||||
content: string,
|
||||
term: string,
|
||||
matches?: readonly FuseResultMatch[],
|
||||
): string {
|
||||
if (!content || !term.trim() || !matches || matches.length === 0) return content;
|
||||
|
||||
if (!content || !term.trim() || !matches || matches.length === 0)
|
||||
return content;
|
||||
|
||||
try {
|
||||
// Find matches for content field or allContent that contains the content
|
||||
const contentMatches = matches.find(match =>
|
||||
match.key === 'content' ||
|
||||
(match.key === 'allContent' && match.value?.includes(content))
|
||||
const contentMatches = matches.find(
|
||||
(match) =>
|
||||
match.key === "content" ||
|
||||
(match.key === "allContent" && match.value?.includes(content)),
|
||||
);
|
||||
|
||||
if (!contentMatches || !contentMatches.indices || contentMatches.indices.length === 0) {
|
||||
|
||||
if (
|
||||
!contentMatches ||
|
||||
!contentMatches.indices ||
|
||||
contentMatches.indices.length === 0
|
||||
) {
|
||||
// No content matches, return plain content
|
||||
return content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||
return content.length > 100 ? content.substring(0, 100) + "..." : content;
|
||||
}
|
||||
|
||||
|
||||
// Find the match indices
|
||||
let allIndices: MatchIndices[] = contentMatches.indices as MatchIndices[];
|
||||
|
||||
|
||||
// If matching against allContent, adjust indices to be relative to content
|
||||
if (contentMatches.key === 'allContent') {
|
||||
if (contentMatches.key === "allContent") {
|
||||
const allContent = contentMatches.value;
|
||||
const contentPos = allContent?.indexOf(content) ?? -1;
|
||||
|
||||
|
||||
if (contentPos >= 0) {
|
||||
// Adjust indices to be relative to the content field
|
||||
allIndices = allIndices
|
||||
.map(indices => [indices[0] - contentPos, indices[1] - contentPos] as MatchIndices)
|
||||
.filter(indices => indices[1] >= 0 && indices[0] < content.length);
|
||||
.map(
|
||||
(indices) =>
|
||||
[
|
||||
indices[0] - contentPos,
|
||||
indices[1] - contentPos,
|
||||
] as MatchIndices,
|
||||
)
|
||||
.filter((indices) => indices[1] >= 0 && indices[0] < content.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (allIndices.length === 0) {
|
||||
return content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||
return content.length > 100 ? content.substring(0, 100) + "..." : content;
|
||||
}
|
||||
|
||||
|
||||
// Find a good center point for our snippet (average of first match)
|
||||
const firstMatch = allIndices[0];
|
||||
const matchCenter = Math.floor((firstMatch[0] + firstMatch[1]) / 2);
|
||||
|
||||
|
||||
// Extract a window around the match
|
||||
const windowSize = 100;
|
||||
const start = Math.max(0, matchCenter - windowSize / 2);
|
||||
const end = Math.min(content.length, matchCenter + windowSize / 2);
|
||||
|
||||
|
||||
// Create the basic snippet
|
||||
let snippet = content.substring(start, end);
|
||||
if (start > 0) snippet = '...' + snippet;
|
||||
if (end < content.length) snippet += '...';
|
||||
|
||||
if (start > 0) snippet = "..." + snippet;
|
||||
if (end < content.length) snippet += "...";
|
||||
|
||||
// Create a highlighting map for the snippet
|
||||
const snippetLength = snippet.length;
|
||||
const highlightMap = new Array(snippetLength).fill(false);
|
||||
|
||||
|
||||
// Calculate offset for the highlighting
|
||||
const startOffset = start > 0 ? start - 3 : start; // Account for '...' if present
|
||||
|
||||
|
||||
// Mark each matched character in the snippet
|
||||
allIndices.forEach((indices: MatchIndices) => {
|
||||
const matchStart = indices[0];
|
||||
const matchEnd = indices[1];
|
||||
|
||||
|
||||
// Skip matches outside our snippet window
|
||||
if (matchEnd < start || matchStart > end) return;
|
||||
|
||||
|
||||
// Adjust match indices to be relative to snippet
|
||||
const snippetMatchStart = Math.max(0, matchStart - startOffset);
|
||||
const snippetMatchEnd = Math.min(snippetLength - 1, matchEnd - startOffset);
|
||||
|
||||
const snippetMatchEnd = Math.min(
|
||||
snippetLength - 1,
|
||||
matchEnd - startOffset,
|
||||
);
|
||||
|
||||
// Mark characters for highlighting
|
||||
for (let i = snippetMatchStart; i <= snippetMatchEnd; i++) {
|
||||
if (i >= 0 && i < snippetLength) {
|
||||
@@ -203,33 +242,33 @@ export function highlightSnippet(
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Build the highlighted snippet
|
||||
let result = '';
|
||||
let result = "";
|
||||
let inHighlight = false;
|
||||
|
||||
|
||||
for (let i = 0; i < snippetLength; i++) {
|
||||
// If highlighting state changes, add appropriate tags
|
||||
if (highlightMap[i] && !inHighlight) {
|
||||
result += '<span class="highlight">';
|
||||
inHighlight = true;
|
||||
} else if (!highlightMap[i] && inHighlight) {
|
||||
result += '</span>';
|
||||
result += "</span>";
|
||||
inHighlight = false;
|
||||
}
|
||||
|
||||
|
||||
// Add the current character
|
||||
result += snippet.charAt(i);
|
||||
}
|
||||
|
||||
|
||||
// Close highlight tag if we're still in one at the end
|
||||
if (inHighlight) {
|
||||
result += '</span>';
|
||||
result += "</span>";
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('Error highlighting snippet:', e);
|
||||
return content.length > 100 ? content.substring(0, 100) + '...' : content;
|
||||
console.error("Error highlighting snippet:", e);
|
||||
return content.length > 100 ? content.substring(0, 100) + "..." : content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user