feat(classes): allow the Blog Editor's own markup in the HTML policy

- figure data-embed ^youtube$ and data-id ^[A-Za-z0-9_-]{11}$ (the embed
  placeholder), img alt with any text, ol start ^[0-9]+$; nothing else
- TestEditorOutputPassesPolicy checks every editor-corpus.json entry byte
  for byte with an empty report, plus the negative cases;
  TestSanitizeHTMLReport pins the removal report
- CheckHTML documented as a predicate no write path refuses with; README
  lists the four additions and the report API
This commit is contained in:
2026-10-10 12:11:29 +02:00
parent cb1c688213
commit 2384a61e38
4 changed files with 292 additions and 8 deletions

View File

@@ -54,6 +54,22 @@ func newHTMLPolicy() *bluemonday.Policy {
p.AllowAttrs("dir").Matching(regexp.MustCompile(`(?i)^(ltr|rtl|auto)$`)).Globally()
p.AllowAttrs("data-message-author-role", "data-message-id").
Matching(regexp.MustCompile(`^[A-Za-z0-9_-]+$`)).OnElements("div")
// Blog Editor output (Phase 16.2 D-05), exactly what the editor writes
// and nothing more; plan 02's editor-corpus.json pins it
// (TestEditorOutputPassesPolicy) and the site layer mirrors these rules.
// The embed placeholder <figure data-embed="youtube" data-id="..."></figure>:
// the provider name and the 11-character video id, on figure only. The
// layer expands it; no iframe is ever stored.
p.AllowAttrs("data-embed").Matching(regexp.MustCompile(`^youtube$`)).OnElements("figure")
p.AllowAttrs("data-id").Matching(regexp.MustCompile(`^[A-Za-z0-9_-]{11}$`)).OnElements("figure")
// The editor's alt field takes free text, which UGC's Paragraph pattern
// cuts at ':', '?', '"' and more. bluemonday accepts an attribute when
// any of its rules matches and escapes the value on output; alt carries
// no URL.
p.AllowAttrs("alt").OnElements("img")
// TipTap's ordered-list input rule writes start when a list does not
// begin at 1.
p.AllowAttrs("start").Matching(regexp.MustCompile(`^[0-9]+$`)).OnElements("ol")
return p
}
@@ -310,10 +326,12 @@ func textOf(v any) string {
}
}
// CheckHTML gates HTML that is stored as is, such as category content. It
// uses the FormatHTML allow-list but refuses instead of stripping: doc is
// refused when the policy would remove or change any element, attribute or
// text in it.
// CheckHTML reports whether the FormatHTML allow-list would remove or
// change any element, attribute or text in doc: it returns an error when it
// would, nil when the policy keeps doc whole. Since Phase 16.2 no admin or
// API write path refuses with it (admin saves strip and report through
// SanitizeHTMLReport, D-10 amended); it is the predicate counterpart of the
// site layer's isSafeJournalHtml, and the policy cross-check tests use it.
func CheckHTML(doc string) error {
if _, changed := sanitizeHTML(doc); changed {
return errors.New("journal: HTML contains elements, attributes or URLs that are not allowed")