fix: decode HTML entities in unlinkifyHtml and fix indentation

- Add entity decoding (& < > ") to unlinkifyHtml to
  prevent double-encoding on contenteditable edit cycles
- Fix indentation of LinkPopover import and state declaration
- Move LinkPopover inside root div
- Add unit test for entity decoding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-04-08 22:36:19 +02:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 377ce4b72c
commit beb4209baf
3 changed files with 19 additions and 8 deletions
+7 -7
View File
@@ -7,7 +7,7 @@
import { dragHandleZone, dragHandle, type DndEvent } from 'svelte-dnd-action';
import { flip } from 'svelte/animate';
import { type ChecklistItem, generateId, parseChecklist, serializeChecklist, toggleItemWithCascade, indentItem, outdentItem, linkifyText, unlinkifyHtml } from '$lib/utils/checklist.js';
import LinkPopover from './LinkPopover.svelte';
import LinkPopover from './LinkPopover.svelte';
interface Props {
content: string;
@@ -20,7 +20,7 @@ import LinkPopover from './LinkPopover.svelte';
let items = $state<ChecklistItem[]>(parseChecklist(content));
let doneExpanded = $state(true);
const flipDurationMs = 150;
let linkPopover = $state<{ url: string; anchor: DOMRect } | null>(null);
let linkPopover = $state<{ url: string; anchor: DOMRect } | null>(null);
// Track the last content we emitted so we can distinguish self-originated
// changes from external ones (sync, history restore, etc.)
@@ -314,7 +314,7 @@ let linkPopover = $state<{ url: string; anchor: DOMRect } | null>(null);
data-testid="checklist-input"
data-item-id={item.id}
role="textbox"
tabindex="0"
tabindex="0"
>{@html linkifyText(item.text)}</div>
<button
onclick={() => removeItem(item.id)}
@@ -382,9 +382,9 @@ let linkPopover = $state<{ url: string; anchor: DOMRect } | null>(null);
{/if}
</div>
{/if}
{#if linkPopover}
<LinkPopover url={linkPopover.url} anchor={linkPopover.anchor} onClose={() => (linkPopover = null)} />
{/if}
</div>
{#if linkPopover}
<LinkPopover url={linkPopover.url} anchor={linkPopover.anchor} onClose={() => (linkPopover = null)} />
{/if}
+6
View File
@@ -312,4 +312,10 @@ describe('unlinkifyHtml', () => {
it('handles div and br tags from contenteditable', () => {
expect(unlinkifyHtml('line1<br>line2')).toBe('line1line2');
});
it('decodes HTML entities after stripping tags', () => {
expect(unlinkifyHtml('Tom &amp; Jerry')).toBe('Tom & Jerry');
expect(unlinkifyHtml('&lt;b&gt;bold&lt;/b&gt;')).toBe('<b>bold</b>');
expect(unlinkifyHtml('say &quot;hello&quot;')).toBe('say "hello"');
});
});
+6 -1
View File
@@ -189,5 +189,10 @@ export function linkifyText(text: string): string {
export function unlinkifyHtml(html: string): string {
if (!html) return '';
return html.replace(/<[^>]*>/g, '');
return html
.replace(/<[^>]*>/g, '')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"');
}