diff --git a/frontend/messages/de.json b/frontend/messages/de.json index bad61019..e08dbb1c 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -919,6 +919,56 @@ "bulk_edit_count_pill": "{count} werden bearbeitet", "nav_stammbaum": "Stammbaum", + "nav_geschichten": "Geschichten", + + "error_geschichte_not_found": "Die Geschichte wurde nicht gefunden.", + + "geschichten_index_title": "Geschichten", + "geschichten_new_button": "Neue Geschichte", + "geschichten_filter_all_pill": "Alle", + "geschichten_filter_choose_person": "Person wählen", + "geschichten_filter_aria_label": "Person filtern", + "geschichten_empty_for_person": "Keine Geschichten für {name} gefunden.", + "geschichten_empty_no_filter": "Es gibt noch keine veröffentlichten Geschichten.", + "geschichten_back_to_index": "Zurück zu Geschichten", + "geschichten_published_on": "veröffentlicht am {date}", + "geschichten_persons_section": "Personen in dieser Geschichte", + "geschichten_documents_section": "Erwähnte Dokumente", + "geschichten_card_heading": "Geschichten", + "geschichten_card_write_action": "+ Geschichte schreiben", + "geschichten_card_attach_action": "+ Geschichte anhängen", + "geschichten_card_show_all_for_person": "Alle Geschichten zu {name}", + "geschichten_card_show_all": "Alle anzeigen", + + "geschichte_editor_title_placeholder": "Titel der Geschichte", + "geschichte_editor_body_placeholder": "Schreibe hier deine Geschichte…", + "geschichte_editor_status_draft": "ENTWURF", + "geschichte_editor_status_published": "VERÖFFENTLICHT", + "geschichte_editor_status_draft_hint": "Noch nicht öffentlich sichtbar.", + "geschichte_editor_status_published_hint": "Öffentlich sichtbar für alle Leser.", + "geschichte_editor_save_hint_draft": "Alle Änderungen werden als Entwurf gespeichert.", + "geschichte_editor_save_hint_published": "Änderungen sind sofort live.", + "geschichte_editor_save_draft": "Entwurf speichern", + "geschichte_editor_publish": "Veröffentlichen", + "geschichte_editor_save": "Speichern", + "geschichte_editor_unpublish": "Zurück zu Entwurf", + "geschichte_editor_title_required": "Bitte gib einen Titel ein.", + "geschichte_editor_unsaved_changes": "Du hast ungespeicherte Änderungen — wirklich verlassen?", + "geschichte_editor_personen_heading": "Personen", + "geschichte_editor_personen_hint": "Welche historischen Personen kommen in dieser Geschichte vor?", + "geschichte_editor_dokumente_heading": "Dokumente", + "geschichte_editor_dokumente_hint": "Welche Briefe oder Dokumente sind Teil dieser Geschichte?", + "geschichte_editor_search_person": "Person suchen…", + "geschichte_editor_search_document": "Dokument suchen…", + "geschichte_editor_toolbar_bold": "Fett (Strg+B)", + "geschichte_editor_toolbar_italic": "Kursiv (Strg+I)", + "geschichte_editor_toolbar_h2": "Überschrift", + "geschichte_editor_toolbar_h3": "Unterüberschrift", + "geschichte_editor_toolbar_ul": "Aufzählung", + "geschichte_editor_toolbar_ol": "Nummerierte Liste", + + "geschichte_delete_confirm_title": "Geschichte löschen?", + "geschichte_delete_confirm_body": "Diese Aktion kann nicht rückgängig gemacht werden. Die Geschichte wird dauerhaft gelöscht und aus allen verlinkten Personen- und Dokumentseiten entfernt.", "error_relationship_not_found": "Die Beziehung wurde nicht gefunden.", "error_circular_relationship": "Diese Beziehung würde einen Kreis erzeugen.", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index ca99ea8f..3e1e691d 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -919,6 +919,56 @@ "bulk_edit_count_pill": "{count} will be edited", "nav_stammbaum": "Family tree", + "nav_geschichten": "Stories", + + "error_geschichte_not_found": "The story was not found.", + + "geschichten_index_title": "Stories", + "geschichten_new_button": "New story", + "geschichten_filter_all_pill": "All", + "geschichten_filter_choose_person": "Choose person", + "geschichten_filter_aria_label": "Filter by person", + "geschichten_empty_for_person": "No stories found for {name}.", + "geschichten_empty_no_filter": "There are no published stories yet.", + "geschichten_back_to_index": "Back to stories", + "geschichten_published_on": "published on {date}", + "geschichten_persons_section": "People in this story", + "geschichten_documents_section": "Referenced documents", + "geschichten_card_heading": "Stories", + "geschichten_card_write_action": "+ Write a story", + "geschichten_card_attach_action": "+ Attach a story", + "geschichten_card_show_all_for_person": "All stories about {name}", + "geschichten_card_show_all": "Show all", + + "geschichte_editor_title_placeholder": "Story title", + "geschichte_editor_body_placeholder": "Write your story here…", + "geschichte_editor_status_draft": "DRAFT", + "geschichte_editor_status_published": "PUBLISHED", + "geschichte_editor_status_draft_hint": "Not yet visible to readers.", + "geschichte_editor_status_published_hint": "Visible to all readers.", + "geschichte_editor_save_hint_draft": "All changes are saved as a draft.", + "geschichte_editor_save_hint_published": "Changes go live immediately.", + "geschichte_editor_save_draft": "Save draft", + "geschichte_editor_publish": "Publish", + "geschichte_editor_save": "Save", + "geschichte_editor_unpublish": "Back to draft", + "geschichte_editor_title_required": "Please enter a title.", + "geschichte_editor_unsaved_changes": "You have unsaved changes — leave anyway?", + "geschichte_editor_personen_heading": "People", + "geschichte_editor_personen_hint": "Which historical persons appear in this story?", + "geschichte_editor_dokumente_heading": "Documents", + "geschichte_editor_dokumente_hint": "Which letters or documents are part of this story?", + "geschichte_editor_search_person": "Search person…", + "geschichte_editor_search_document": "Search document…", + "geschichte_editor_toolbar_bold": "Bold (Ctrl+B)", + "geschichte_editor_toolbar_italic": "Italic (Ctrl+I)", + "geschichte_editor_toolbar_h2": "Heading", + "geschichte_editor_toolbar_h3": "Subheading", + "geschichte_editor_toolbar_ul": "Bulleted list", + "geschichte_editor_toolbar_ol": "Numbered list", + + "geschichte_delete_confirm_title": "Delete story?", + "geschichte_delete_confirm_body": "This action cannot be undone. The story will be permanently deleted and removed from all linked person and document pages.", "error_relationship_not_found": "Relationship not found.", "error_circular_relationship": "This relationship would form a cycle.", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 3630d405..a1cff1eb 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -919,6 +919,56 @@ "bulk_edit_count_pill": "Se editarán {count}", "nav_stammbaum": "Árbol genealógico", + "nav_geschichten": "Historias", + + "error_geschichte_not_found": "No se encontró la historia.", + + "geschichten_index_title": "Historias", + "geschichten_new_button": "Nueva historia", + "geschichten_filter_all_pill": "Todas", + "geschichten_filter_choose_person": "Elegir persona", + "geschichten_filter_aria_label": "Filtrar por persona", + "geschichten_empty_for_person": "No hay historias para {name}.", + "geschichten_empty_no_filter": "Aún no hay historias publicadas.", + "geschichten_back_to_index": "Volver a Historias", + "geschichten_published_on": "publicada el {date}", + "geschichten_persons_section": "Personas en esta historia", + "geschichten_documents_section": "Documentos mencionados", + "geschichten_card_heading": "Historias", + "geschichten_card_write_action": "+ Escribir historia", + "geschichten_card_attach_action": "+ Adjuntar historia", + "geschichten_card_show_all_for_person": "Todas las historias sobre {name}", + "geschichten_card_show_all": "Mostrar todas", + + "geschichte_editor_title_placeholder": "Título de la historia", + "geschichte_editor_body_placeholder": "Escribe tu historia aquí…", + "geschichte_editor_status_draft": "BORRADOR", + "geschichte_editor_status_published": "PUBLICADA", + "geschichte_editor_status_draft_hint": "Aún no visible para lectores.", + "geschichte_editor_status_published_hint": "Visible para todos los lectores.", + "geschichte_editor_save_hint_draft": "Los cambios se guardan como borrador.", + "geschichte_editor_save_hint_published": "Los cambios se publican inmediatamente.", + "geschichte_editor_save_draft": "Guardar borrador", + "geschichte_editor_publish": "Publicar", + "geschichte_editor_save": "Guardar", + "geschichte_editor_unpublish": "Volver a borrador", + "geschichte_editor_title_required": "Por favor ingresa un título.", + "geschichte_editor_unsaved_changes": "Tienes cambios no guardados — ¿salir igualmente?", + "geschichte_editor_personen_heading": "Personas", + "geschichte_editor_personen_hint": "¿Qué personas históricas aparecen en esta historia?", + "geschichte_editor_dokumente_heading": "Documentos", + "geschichte_editor_dokumente_hint": "¿Qué cartas o documentos forman parte de esta historia?", + "geschichte_editor_search_person": "Buscar persona…", + "geschichte_editor_search_document": "Buscar documento…", + "geschichte_editor_toolbar_bold": "Negrita (Ctrl+B)", + "geschichte_editor_toolbar_italic": "Cursiva (Ctrl+I)", + "geschichte_editor_toolbar_h2": "Encabezado", + "geschichte_editor_toolbar_h3": "Subencabezado", + "geschichte_editor_toolbar_ul": "Lista con viñetas", + "geschichte_editor_toolbar_ol": "Lista numerada", + + "geschichte_delete_confirm_title": "¿Eliminar historia?", + "geschichte_delete_confirm_body": "Esta acción no se puede deshacer. La historia se eliminará permanentemente y se quitará de todas las páginas de personas y documentos vinculados.", "error_relationship_not_found": "La relación no fue encontrada.", "error_circular_relationship": "Esta relación crearía un ciclo.", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d54afd27..b8a2abc7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,7 +12,7 @@ "@tiptap/extension-mention": "3.22.5", "@tiptap/starter-kit": "3.22.5", "diff": "^8.0.3", - "dompurify": "^3.4.2", + "isomorphic-dompurify": "^3.12.0", "openapi-fetch": "^0.13.5", "pdfjs-dist": "^5.5.207" }, @@ -29,7 +29,6 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", "@types/diff": "^7.0.2", - "@types/dompurify": "^3.0.5", "@types/node": "^24", "@vitest/browser-playwright": "^4.0.10", "@vitest/coverage-v8": "^4.1.0", @@ -53,6 +52,53 @@ "vitest-browser-svelte": "^2.0.1" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, "node_modules/@axe-core/playwright": { "version": "4.11.1", "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz", @@ -148,6 +194,152 @@ "dev": true, "license": "MIT" }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", @@ -768,6 +960,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2622,16 +2831,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dompurify": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/trusted-types": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3319,6 +3518,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3507,6 +3715,19 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3520,6 +3741,19 @@ "node": ">=4" } }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3538,6 +3772,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -3619,6 +3859,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-module-lexer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", @@ -4105,6 +4357,18 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4232,6 +4496,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -4249,6 +4519,19 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-dompurify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.12.0.tgz", + "integrity": "sha512-8n+j+6ypTHvriJwFOQ2qusQ6bzGjZVcR3jbe1pBpLcGI1dn4WIl0ctLBngqE5QttquQBAlKXwJeTMw+X7x7qKw==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.4.2", + "jsdom": "^29.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -4335,6 +4618,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4727,6 +5050,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4765,6 +5097,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -4995,6 +5333,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5500,7 +5850,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5524,7 +5873,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5625,6 +5973,18 @@ "node": ">=6" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -5694,7 +6054,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5872,6 +6231,12 @@ "@types/estree": "^1.0.6" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", @@ -5937,6 +6302,24 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "license": "MIT" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -5947,6 +6330,30 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -6024,6 +6431,15 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -6335,6 +6751,27 @@ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -6342,6 +6779,29 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6407,6 +6867,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, "node_modules/yaml-ast-parser": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", diff --git a/frontend/package.json b/frontend/package.json index ad6083cd..34ef5804 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,7 @@ "@tiptap/extension-mention": "3.22.5", "@tiptap/starter-kit": "3.22.5", "diff": "^8.0.3", - "dompurify": "^3.4.2", + "isomorphic-dompurify": "^3.12.0", "openapi-fetch": "^0.13.5", "pdfjs-dist": "^5.5.207" }, @@ -42,7 +42,6 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", "@types/diff": "^7.0.2", - "@types/dompurify": "^3.0.5", "@types/node": "^24", "@vitest/browser-playwright": "^4.0.10", "@vitest/coverage-v8": "^4.1.0", diff --git a/frontend/src/lib/errors.ts b/frontend/src/lib/errors.ts index e57f212e..cbb137c2 100644 --- a/frontend/src/lib/errors.ts +++ b/frontend/src/lib/errors.ts @@ -41,6 +41,7 @@ export type ErrorCode = | 'RELATIONSHIP_NOT_FOUND' | 'CIRCULAR_RELATIONSHIP' | 'DUPLICATE_RELATIONSHIP' + | 'GESCHICHTE_NOT_FOUND' | 'MISSING_CREDENTIALS' | 'UNAUTHORIZED' | 'FORBIDDEN' @@ -145,6 +146,8 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string { return m.error_circular_relationship(); case 'DUPLICATE_RELATIONSHIP': return m.error_duplicate_relationship(); + case 'GESCHICHTE_NOT_FOUND': + return m.error_geschichte_not_found(); case 'MISSING_CREDENTIALS': return m.login_error_missing_credentials(); case 'UNAUTHORIZED': diff --git a/frontend/src/lib/utils/sanitize.spec.ts b/frontend/src/lib/utils/sanitize.spec.ts new file mode 100644 index 00000000..9495063d --- /dev/null +++ b/frontend/src/lib/utils/sanitize.spec.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { safeHtml } from './sanitize'; + +describe('safeHtml', () => { + it('returns empty string for null/undefined/empty input', () => { + expect(safeHtml(null)).toBe(''); + expect(safeHtml(undefined)).toBe(''); + expect(safeHtml('')).toBe(''); + }); + + it('keeps allowed tags: p, strong, em, br, h2, h3, ul, ol, li', () => { + const html = + '
bold italic
x