{
+ it('returns full text when under the limit', () => {
+ expect(plainExcerpt('short
', 80)).toBe('short');
+ });
+
+ it('truncates at the boundary with an ellipsis', () => {
+ const html = '' + 'a'.repeat(100) + '
';
+ const out = plainExcerpt(html, 20);
+ expect(out.length).toBeLessThanOrEqual(21);
+ expect(out.endsWith('…')).toBe(true);
+ });
+
+ it('breaks at a word boundary when possible', () => {
+ const out = plainExcerpt('The quick brown fox jumps over
', 18);
+ expect(out).toBe('The quick brown…');
+ });
+});
diff --git a/frontend/src/lib/utils/extractText.ts b/frontend/src/lib/utils/extractText.ts
new file mode 100644
index 00000000..331d9dea
--- /dev/null
+++ b/frontend/src/lib/utils/extractText.ts
@@ -0,0 +1,38 @@
+/**
+ * **Not a sanitizer.** This module extracts visible text from a (presumed
+ * already-sanitised) HTML string for excerpt rendering. It is safe ONLY
+ * because the Geschichte body is sanitised against the OWASP allow-list
+ * on the server before persistence, and via DOMPurify on render.
+ *
+ * Do not use these helpers to defend against XSS — `safeHtml()` in
+ * `./sanitize.ts` is the only sanitiser. Calling `extractText()` on
+ * untrusted input that has not been sanitised does not protect against
+ * `javascript:` URLs, event-handler attributes, or `