feat(voyage): vendor DOMPurify >=3.1.1 + sanitize annotation-content

This commit is contained in:
Kjell Tore Guttormsen 2026-05-10 18:01:30 +02:00
commit fc8c9eecdd
5 changed files with 105 additions and 6 deletions

View file

@ -1,9 +1,10 @@
{
"generated_at": "2026-05-09T13:16:03.483Z",
"generated_at": "2026-05-10T15:59:53.379Z",
"pins": {
"markdown-it": "14.1.0",
"markdown-it-front-matter": "0.2.4",
"highlight.js": "11.11.1"
"highlight.js": "11.11.1",
"dompurify": "3.2.6"
},
"highlight_languages": [
"yaml",
@ -16,6 +17,7 @@
"output_files": [
"markdown-it.min.js",
"markdown-it-front-matter.min.js",
"highlight.min.js"
"highlight.min.js",
"dompurify.min.js"
]
}

File diff suppressed because one or more lines are too long

View file

@ -1135,6 +1135,10 @@
<script src="lib/markdown-it.min.js"></script>
<script src="lib/markdown-it-front-matter.min.js"></script>
<script src="lib/highlight.min.js"></script>
<!-- v4.3 Step 24 — DOMPurify ≥ 3.1.1 (UMD bundle exposes window.DOMPurify).
Used by sanitizeAnnotation() to scrub annotation rich-text before DOM
insertion. Pinned via scripts/vendor-playground-libs.mjs. -->
<script src="lib/dompurify.min.js"></script>
<!-- Sample plan inlined for Step 8 first-run experience.
Same content as tests/fixtures/annotation/annotation-plan.md (truncated). -->
@ -1244,6 +1248,27 @@ playground first-run shows a complete round-trip-able artifact.
.replace(/'/g, '&#39;');
}
// v4.3 Step 24 — DOMPurify-backed annotation-content sanitizer.
// Strips <script>, inline styles, and event-handler attributes; keeps
// a small allowlist of inline-formatting tags so users can paste basic
// rich-text (bold/italic/code) without breaking export round-trip.
// Falls back to escapeHtml when DOMPurify is unavailable (file:// in a
// browser without the vendored bundle, or test environments).
function sanitizeAnnotation(html) {
var input = String(html == null ? '' : html);
if (typeof window !== 'undefined' && window.DOMPurify && typeof window.DOMPurify.sanitize === 'function') {
return window.DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'code'],
ALLOWED_ATTR: [],
FORBID_TAGS: ['style', 'script'],
FORBID_ATTR: ['style', 'onerror', 'onload'],
});
}
// Defensive fallback — never inject untrusted HTML if DOMPurify
// failed to load.
return escapeHtml(input);
}
// Parse frontmatter yourself (cheap line-walk) so deriveStorageKey can
// see slug/type without wire-tapping the plugin.
function quickParseFrontmatter(text) {
@ -2306,7 +2331,11 @@ playground first-run shows a complete round-trip-able artifact.
(annot.line ? '<span class="critique-card__line">linje ' + annot.line + '</span>' : '') +
'</div>' +
(annot.snippet ? '<div class="critique-card__snippet">' + escapeHtmlInline(annot.snippet) + '</div>' : '') +
'<div class="critique-card__comment">' + escapeHtmlInline(annot.comment || '') + '</div>' +
// v4.3 Step 24 — comment is user-entered rich-text; route through
// sanitizeAnnotation (DOMPurify-backed) so basic inline-formatting
// tags survive while <script>, inline styles, and event-handler
// attributes are stripped before DOM insertion.
'<div class="critique-card__comment">' + sanitizeAnnotation(annot.comment || '') + '</div>' +
'<div class="critique-card__status' + (annot.exported ? ' critique-card__status--exported' : '') + '">' +
(annot.exported ? 'Eksportert' : 'Pending') +
'</div>';