mirror of
https://github.com/dergigi/boris.git
synced 2026-02-17 21:15:02 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8faa2e2de0 | ||
|
|
07a5826774 | ||
|
|
21d6916ae3 | ||
|
|
482ba9b2df | ||
|
|
e4b6d1a122 | ||
|
|
b59a295ad3 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "boris",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -130,6 +130,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
selectedUrl={selectedUrl}
|
||||
highlights={highlights}
|
||||
showUnderlines={showUnderlines}
|
||||
highlightStyle={settings.highlightStyle || 'marker'}
|
||||
highlightColor={settings.highlightColor || '#ffff00'}
|
||||
onHighlightClick={(id) => {
|
||||
setSelectedHighlightId(id)
|
||||
if (isHighlightsCollapsed) setIsHighlightsCollapsed(false)
|
||||
|
||||
26
src/components/ColorPicker.tsx
Normal file
26
src/components/ColorPicker.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { HIGHLIGHT_COLORS } from '../utils/colorHelpers'
|
||||
|
||||
interface ColorPickerProps {
|
||||
selectedColor: string
|
||||
onColorChange: (color: string) => void
|
||||
}
|
||||
|
||||
const ColorPicker: React.FC<ColorPickerProps> = ({ selectedColor, onColorChange }) => {
|
||||
return (
|
||||
<div className="color-picker">
|
||||
{HIGHLIGHT_COLORS.map(color => (
|
||||
<button
|
||||
key={color.value}
|
||||
onClick={() => onColorChange(color.value)}
|
||||
className={`color-swatch ${selectedColor === color.value ? 'active' : ''}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
aria-label={`${color.name} highlight color`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorPicker
|
||||
@@ -7,6 +7,7 @@ import { Highlight } from '../types/highlights'
|
||||
import { applyHighlightsToHTML } from '../utils/highlightMatching'
|
||||
import { readingTime } from 'reading-time-estimator'
|
||||
import { filterHighlightsByUrl } from '../utils/urlHelpers'
|
||||
import { hexToRgb } from '../utils/colorHelpers'
|
||||
|
||||
interface ContentPanelProps {
|
||||
loading: boolean
|
||||
@@ -16,6 +17,8 @@ interface ContentPanelProps {
|
||||
selectedUrl?: string
|
||||
highlights?: Highlight[]
|
||||
showUnderlines?: boolean
|
||||
highlightStyle?: 'marker' | 'underline'
|
||||
highlightColor?: string
|
||||
onHighlightClick?: (highlightId: string) => void
|
||||
selectedHighlightId?: string
|
||||
}
|
||||
@@ -28,6 +31,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
selectedUrl,
|
||||
highlights = [],
|
||||
showUnderlines = true,
|
||||
highlightStyle = 'marker',
|
||||
highlightColor = '#ffff00',
|
||||
onHighlightClick,
|
||||
selectedHighlightId
|
||||
}) => {
|
||||
@@ -38,7 +43,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
useEffect(() => {
|
||||
if (!selectedHighlightId || !contentRef.current) return
|
||||
|
||||
const markElement = contentRef.current.querySelector(`mark.content-highlight[data-highlight-id="${selectedHighlightId}"]`)
|
||||
const markElement = contentRef.current.querySelector(`mark[data-highlight-id="${selectedHighlightId}"]`)
|
||||
|
||||
if (markElement) {
|
||||
markElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
@@ -86,18 +91,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
if (!contentRef.current || !originalHtmlRef.current) return
|
||||
|
||||
// Always apply highlights to the ORIGINAL HTML, not already-highlighted content
|
||||
const highlightedHTML = applyHighlightsToHTML(originalHtmlRef.current, relevantHighlights)
|
||||
const highlightedHTML = applyHighlightsToHTML(originalHtmlRef.current, relevantHighlights, highlightStyle)
|
||||
contentRef.current.innerHTML = highlightedHTML
|
||||
})
|
||||
|
||||
return () => cancelAnimationFrame(rafId)
|
||||
}, [relevantHighlights, html, markdown, showUnderlines])
|
||||
}, [relevantHighlights, html, markdown, showUnderlines, highlightStyle])
|
||||
|
||||
// Attach click handlers separately (only when handler changes)
|
||||
useEffect(() => {
|
||||
if (!onHighlightClick || !contentRef.current) return
|
||||
|
||||
const marks = contentRef.current.querySelectorAll('mark.content-highlight')
|
||||
const marks = contentRef.current.querySelectorAll('mark[data-highlight-id]')
|
||||
const handlers = new Map<Element, () => void>()
|
||||
|
||||
marks.forEach(mark => {
|
||||
@@ -149,8 +154,10 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const highlightRgb = hexToRgb(highlightColor)
|
||||
|
||||
return (
|
||||
<div className="reader">
|
||||
<div className="reader" style={{ '--highlight-rgb': highlightRgb } as React.CSSProperties}>
|
||||
{title && (
|
||||
<div className="reader-header">
|
||||
<h2 className="reader-title">{title}</h2>
|
||||
|
||||
38
src/components/FontSelector.tsx
Normal file
38
src/components/FontSelector.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
interface FontSelectorProps {
|
||||
value: string
|
||||
onChange: (font: string) => void
|
||||
}
|
||||
|
||||
const FONTS = [
|
||||
{ value: 'system', label: 'System Default', family: 'system-ui, -apple-system, sans-serif' },
|
||||
{ value: 'inter', label: 'Inter', family: 'Inter, sans-serif' },
|
||||
{ value: 'lora', label: 'Lora', family: 'Lora, serif' },
|
||||
{ value: 'merriweather', label: 'Merriweather', family: 'Merriweather, serif' },
|
||||
{ value: 'open-sans', label: 'Open Sans', family: 'Open Sans, sans-serif' },
|
||||
{ value: 'roboto', label: 'Roboto', family: 'Roboto, sans-serif' },
|
||||
{ value: 'source-serif-4', label: 'Source Serif 4', family: 'Source Serif 4, serif' },
|
||||
{ value: 'crimson-text', label: 'Crimson Text', family: 'Crimson Text, serif' },
|
||||
{ value: 'libre-baskerville', label: 'Libre Baskerville', family: 'Libre Baskerville, serif' },
|
||||
{ value: 'pt-serif', label: 'PT Serif', family: 'PT Serif, serif' }
|
||||
]
|
||||
|
||||
const FontSelector: React.FC<FontSelectorProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<select
|
||||
id="readingFont"
|
||||
value={value || 'system'}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="setting-select font-select"
|
||||
>
|
||||
{FONTS.map(font => (
|
||||
<option key={font.value} value={font.value} style={{ fontFamily: font.family }}>
|
||||
{font.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export default FontSelector
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { faTimes, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faTimes, faList, faThLarge, faImage, faUnderline, faHighlighter } from '@fortawesome/free-solid-svg-icons'
|
||||
import { UserSettings } from '../services/settingsService'
|
||||
import IconButton from './IconButton'
|
||||
import ColorPicker from './ColorPicker'
|
||||
import FontSelector from './FontSelector'
|
||||
import { loadFont, getFontFamily } from '../utils/fontLoader'
|
||||
import { hexToRgb } from '../utils/colorHelpers'
|
||||
|
||||
interface SettingsProps {
|
||||
settings: UserSettings
|
||||
@@ -62,23 +65,10 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label htmlFor="readingFont">Reading Font</label>
|
||||
<select
|
||||
id="readingFont"
|
||||
<FontSelector
|
||||
value={localSettings.readingFont || 'system'}
|
||||
onChange={(e) => setLocalSettings({ ...localSettings, readingFont: e.target.value })}
|
||||
className="setting-select font-select"
|
||||
>
|
||||
<option value="system" style={{ fontFamily: 'system-ui, -apple-system, sans-serif' }}>System Default</option>
|
||||
<option value="inter" style={{ fontFamily: 'Inter, sans-serif' }}>Inter</option>
|
||||
<option value="lora" style={{ fontFamily: 'Lora, serif' }}>Lora</option>
|
||||
<option value="merriweather" style={{ fontFamily: 'Merriweather, serif' }}>Merriweather</option>
|
||||
<option value="open-sans" style={{ fontFamily: 'Open Sans, sans-serif' }}>Open Sans</option>
|
||||
<option value="roboto" style={{ fontFamily: 'Roboto, sans-serif' }}>Roboto</option>
|
||||
<option value="source-serif-4" style={{ fontFamily: 'Source Serif 4, serif' }}>Source Serif 4</option>
|
||||
<option value="crimson-text" style={{ fontFamily: 'Crimson Text, serif' }}>Crimson Text</option>
|
||||
<option value="libre-baskerville" style={{ fontFamily: 'Libre Baskerville, serif' }}>Libre Baskerville</option>
|
||||
<option value="pt-serif" style={{ fontFamily: 'PT Serif, serif' }}>PT Serif</option>
|
||||
</select>
|
||||
onChange={(font) => setLocalSettings({ ...localSettings, readingFont: font })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
@@ -111,17 +101,46 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Highlight Style</label>
|
||||
<div className="setting-buttons">
|
||||
<IconButton
|
||||
icon={faHighlighter}
|
||||
onClick={() => setLocalSettings({ ...localSettings, highlightStyle: 'marker' })}
|
||||
title="Text marker style"
|
||||
ariaLabel="Text marker style"
|
||||
variant={(localSettings.highlightStyle || 'marker') === 'marker' ? 'primary' : 'ghost'}
|
||||
/>
|
||||
<IconButton
|
||||
icon={faUnderline}
|
||||
onClick={() => setLocalSettings({ ...localSettings, highlightStyle: 'underline' })}
|
||||
title="Underline style"
|
||||
ariaLabel="Underline style"
|
||||
variant={localSettings.highlightStyle === 'underline' ? 'primary' : 'ghost'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-group setting-inline">
|
||||
<label>Highlight Color</label>
|
||||
<ColorPicker
|
||||
selectedColor={localSettings.highlightColor || '#ffff00'}
|
||||
onColorChange={(color) => setLocalSettings({ ...localSettings, highlightColor: color })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="setting-preview">
|
||||
<div className="preview-label">Preview</div>
|
||||
<div
|
||||
className="preview-content"
|
||||
style={{
|
||||
fontFamily: previewFontFamily,
|
||||
fontSize: `${localSettings.fontSize || 16}px`
|
||||
}}
|
||||
fontSize: `${localSettings.fontSize || 16}px`,
|
||||
'--highlight-rgb': hexToRgb(localSettings.highlightColor || '#ffff00')
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<h3>The Quick Brown Fox</h3>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={localSettings.showUnderlines !== false ? "content-highlight" : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam.</p>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={localSettings.showUnderlines !== false ? `content-highlight-${localSettings.highlightStyle || 'marker'}` : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam.</p>
|
||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
123
src/index.css
123
src/index.css
@@ -1402,60 +1402,102 @@ body {
|
||||
}
|
||||
|
||||
/* Inline content highlights - fluorescent marker style */
|
||||
.content-highlight {
|
||||
background: rgba(255, 255, 0, 0.35);
|
||||
.content-highlight,
|
||||
.content-highlight-marker {
|
||||
background: rgba(var(--highlight-rgb, 255, 255, 0), 0.35);
|
||||
padding: 0.125rem 0.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
|
||||
box-shadow: 0 0 8px rgba(var(--highlight-rgb, 255, 255, 0), 0.2);
|
||||
}
|
||||
|
||||
.content-highlight:hover {
|
||||
background: rgba(255, 255, 0, 0.5);
|
||||
box-shadow: 0 0 12px rgba(255, 255, 0, 0.3);
|
||||
.content-highlight:hover,
|
||||
.content-highlight-marker:hover {
|
||||
background: rgba(var(--highlight-rgb, 255, 255, 0), 0.5);
|
||||
box-shadow: 0 0 12px rgba(var(--highlight-rgb, 255, 255, 0), 0.3);
|
||||
}
|
||||
|
||||
.content-highlight.highlight-pulse {
|
||||
/* Underline style for highlights */
|
||||
.content-highlight-underline {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 0.8);
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.content-highlight-underline:hover {
|
||||
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 1);
|
||||
text-decoration-thickness: 3px;
|
||||
}
|
||||
|
||||
.content-highlight.highlight-pulse,
|
||||
.content-highlight-marker.highlight-pulse,
|
||||
.content-highlight-underline.highlight-pulse {
|
||||
animation: highlight-pulse-animation 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse-animation {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
|
||||
box-shadow: 0 0 8px rgba(var(--highlight-rgb, 255, 255, 0), 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 20px rgba(255, 255, 0, 0.6);
|
||||
box-shadow: 0 0 20px rgba(var(--highlight-rgb, 255, 255, 0), 0.6);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
|
||||
box-shadow: 0 0 8px rgba(var(--highlight-rgb, 255, 255, 0), 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 20px rgba(255, 255, 0, 0.6);
|
||||
box-shadow: 0 0 20px rgba(var(--highlight-rgb, 255, 255, 0), 0.6);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.reader-html .content-highlight,
|
||||
.reader-markdown .content-highlight {
|
||||
.reader-markdown .content-highlight,
|
||||
.reader-html .content-highlight-marker,
|
||||
.reader-markdown .content-highlight-marker,
|
||||
.reader-html .content-highlight-underline,
|
||||
.reader-markdown .content-highlight-underline {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.reader-html .content-highlight,
|
||||
.reader-markdown .content-highlight,
|
||||
.reader-html .content-highlight-marker,
|
||||
.reader-markdown .content-highlight-marker {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Ensure highlights work in both light and dark mode */
|
||||
@media (prefers-color-scheme: light) {
|
||||
.content-highlight {
|
||||
background: rgba(255, 255, 0, 0.4);
|
||||
box-shadow: 0 0 6px rgba(255, 255, 0, 0.15);
|
||||
.content-highlight,
|
||||
.content-highlight-marker {
|
||||
background: rgba(var(--highlight-rgb, 255, 255, 0), 0.4);
|
||||
box-shadow: 0 0 6px rgba(var(--highlight-rgb, 255, 255, 0), 0.15);
|
||||
}
|
||||
|
||||
.content-highlight:hover {
|
||||
background: rgba(255, 255, 0, 0.55);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 0, 0.25);
|
||||
.content-highlight:hover,
|
||||
.content-highlight-marker:hover {
|
||||
background: rgba(var(--highlight-rgb, 255, 255, 0), 0.55);
|
||||
box-shadow: 0 0 10px rgba(var(--highlight-rgb, 255, 255, 0), 0.25);
|
||||
}
|
||||
|
||||
.content-highlight-underline {
|
||||
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 0.9);
|
||||
}
|
||||
|
||||
.content-highlight-underline:hover {
|
||||
text-decoration-color: rgba(var(--highlight-rgb, 255, 255, 0), 1);
|
||||
}
|
||||
|
||||
.highlight-indicator {
|
||||
@@ -1545,13 +1587,50 @@ body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.color-swatch:hover {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.color-swatch.active {
|
||||
border-color: #646cff;
|
||||
box-shadow: 0 0 0 2px #646cff;
|
||||
}
|
||||
|
||||
.color-swatch.active::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #000;
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.font-size-btn {
|
||||
min-width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem;
|
||||
min-width: 33px;
|
||||
height: 33px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid #444;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface UserSettings {
|
||||
highlightsCollapsed?: boolean
|
||||
readingFont?: string
|
||||
fontSize?: number
|
||||
highlightStyle?: 'marker' | 'underline'
|
||||
highlightColor?: string
|
||||
}
|
||||
|
||||
export async function loadSettings(
|
||||
|
||||
16
src/utils/colorHelpers.ts
Normal file
16
src/utils/colorHelpers.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Helper to convert hex color to RGB values
|
||||
export function hexToRgb(hex: string): string {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
|
||||
: '255, 255, 0'
|
||||
}
|
||||
|
||||
export const HIGHLIGHT_COLORS = [
|
||||
{ name: 'Yellow', value: '#ffff00' },
|
||||
{ name: 'Orange', value: '#ff9500' },
|
||||
{ name: 'Pink', value: '#ff69b4' },
|
||||
{ name: 'Green', value: '#00ff7f' },
|
||||
{ name: 'Blue', value: '#4da6ff' },
|
||||
{ name: 'Purple', value: '#b19cd9' }
|
||||
]
|
||||
@@ -99,9 +99,9 @@ export function applyHighlightsToText(
|
||||
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()
|
||||
|
||||
// Helper to create a mark element for a highlight
|
||||
function createMarkElement(highlight: Highlight, matchText: string): HTMLElement {
|
||||
function createMarkElement(highlight: Highlight, matchText: string, highlightStyle: 'marker' | 'underline' = 'marker'): HTMLElement {
|
||||
const mark = document.createElement('mark')
|
||||
mark.className = 'content-highlight'
|
||||
mark.className = `content-highlight-${highlightStyle}`
|
||||
mark.setAttribute('data-highlight-id', highlight.id)
|
||||
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
|
||||
mark.textContent = matchText
|
||||
@@ -127,7 +127,8 @@ function tryMarkInTextNodes(
|
||||
textNodes: Text[],
|
||||
searchText: string,
|
||||
highlight: Highlight,
|
||||
useNormalized: boolean
|
||||
useNormalized: boolean,
|
||||
highlightStyle: 'marker' | 'underline' = 'marker'
|
||||
): boolean {
|
||||
const normalizedSearch = normalizeWhitespace(searchText)
|
||||
|
||||
@@ -154,7 +155,7 @@ function tryMarkInTextNodes(
|
||||
const before = text.substring(0, actualIndex)
|
||||
const match = text.substring(actualIndex, actualIndex + searchText.length)
|
||||
const after = text.substring(actualIndex + searchText.length)
|
||||
const mark = createMarkElement(highlight, match)
|
||||
const mark = createMarkElement(highlight, match, highlightStyle)
|
||||
|
||||
replaceTextWithMark(textNode, before, after, mark)
|
||||
return true
|
||||
@@ -166,7 +167,7 @@ function tryMarkInTextNodes(
|
||||
/**
|
||||
* Apply highlights to HTML content by injecting mark tags using DOM manipulation
|
||||
*/
|
||||
export function applyHighlightsToHTML(html: string, highlights: Highlight[]): string {
|
||||
export function applyHighlightsToHTML(html: string, highlights: Highlight[], highlightStyle: 'marker' | 'underline' = 'marker'): string {
|
||||
if (!html || highlights.length === 0) return html
|
||||
|
||||
const tempDiv = document.createElement('div')
|
||||
@@ -183,8 +184,8 @@ export function applyHighlightsToHTML(html: string, highlights: Highlight[]): st
|
||||
while ((node = walker.nextNode())) textNodes.push(node as Text)
|
||||
|
||||
// Try exact match first, then normalized match
|
||||
tryMarkInTextNodes(textNodes, searchText, highlight, false) ||
|
||||
tryMarkInTextNodes(textNodes, searchText, highlight, true)
|
||||
tryMarkInTextNodes(textNodes, searchText, highlight, false, highlightStyle) ||
|
||||
tryMarkInTextNodes(textNodes, searchText, highlight, true, highlightStyle)
|
||||
}
|
||||
|
||||
return tempDiv.innerHTML
|
||||
|
||||
Reference in New Issue
Block a user