This repository has no description
1package sanitizer
2
3import (
4 "maps"
5 "regexp"
6 "slices"
7 "strings"
8
9 "github.com/alecthomas/chroma/v2"
10 "github.com/microcosm-cc/bluemonday"
11)
12
13// shared policies built once at init; safe for concurrent use per bluemonday docs
14var (
15 sharedDefaultPolicy *bluemonday.Policy
16 sharedDescriptionPolicy *bluemonday.Policy
17 sharedLogsPolicy *bluemonday.Policy
18)
19
20func init() {
21 sharedDefaultPolicy = buildDefaultPolicy()
22 sharedDescriptionPolicy = buildDescriptionPolicy()
23 sharedLogsPolicy = buildLogsPolicy()
24}
25
26func SanitizeDefault(html string) string {
27 return sharedDefaultPolicy.Sanitize(html)
28}
29func SanitizeDescription(html string) string {
30 return sharedDescriptionPolicy.Sanitize(html)
31}
32func SanitizeLogs(html string) string {
33 return sharedLogsPolicy.Sanitize(html)
34}
35
36func buildDefaultPolicy() *bluemonday.Policy {
37 policy := bluemonday.UGCPolicy()
38
39 // Allow generally safe attributes
40 generalSafeAttrs := []string{
41 "abbr", "accept", "accept-charset",
42 "accesskey", "action", "align", "alt",
43 "aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
44 "axis", "border", "cellpadding", "cellspacing", "char",
45 "charoff", "charset", "checked",
46 "clear", "cols", "colspan", "color",
47 "compact", "coords", "datetime", "dir",
48 "disabled", "enctype", "for", "frame",
49 "headers", "height", "hreflang",
50 "hspace", "ismap", "label", "lang",
51 "maxlength", "media", "method",
52 "multiple", "name", "nohref", "noshade",
53 "nowrap", "open", "prompt", "readonly", "rel", "rev",
54 "rows", "rowspan", "rules", "scope",
55 "selected", "shape", "size", "span",
56 "start", "summary", "tabindex", "target",
57 "title", "type", "usemap", "valign", "value",
58 "vspace", "width", "itemprop",
59 }
60
61 generalSafeElements := []string{
62 "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
63 "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
64 "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
65 "details", "caption", "figure", "figcaption",
66 "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
67 }
68
69 policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
70
71 // video
72 policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
73
74 // picture/source for modern image formats (avif, webp, etc.)
75 policy.AllowAttrs("srcset", "type", "media").OnElements("source")
76
77 // checkboxes
78 policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
79 policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
80
81 // for code blocks
82 policy.AllowAttrs("class").Matching(regexp.MustCompile(`chroma|mermaid`)).OnElements("pre")
83 policy.AllowAttrs("class").Matching(regexp.MustCompile(`anchor|footnote-ref|footnote-backref`)).OnElements("a")
84 policy.AllowAttrs("class").Matching(regexp.MustCompile(`heading`)).OnElements("h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8")
85 policy.AllowAttrs("class").Matching(regexp.MustCompile(strings.Join(slices.Collect(maps.Values(chroma.StandardTypes)), "|"))).OnElements("span")
86
87 // at-mentions
88 policy.AllowAttrs("class").Matching(regexp.MustCompile(`mention`)).OnElements("a")
89
90 // centering content
91 policy.AllowElements("center")
92
93 policy.AllowAttrs("align", "style", "width", "height").Globally()
94 policy.AllowStyles(
95 "margin",
96 "padding",
97 "text-align",
98 "font-weight",
99 "text-decoration",
100 "padding-left",
101 "padding-right",
102 "padding-top",
103 "padding-bottom",
104 "margin-left",
105 "margin-right",
106 "margin-top",
107 "margin-bottom",
108 )
109
110 // math: the math extension emits <span class="math inline|display"> wrapping
111 // the raw LaTeX (delimited by \( \) / \[ \]). MathJax renders it client-side,
112 // so the sanitizer only needs to preserve these carrier spans.
113 policy.AllowAttrs("class").Matching(regexp.MustCompile(`^math (inline|display)$`)).OnElements("span")
114
115 // raw MathML: markdown is rendered with html.WithUnsafe(), so hand-authored
116 // <math>...</math> in source passes through to here. Browsers render
117 // presentation MathML natively, so preserve the elements and their attributes.
118 mathAttrs := []string{
119 "accent", "columnalign", "columnlines", "columnspan", "dir", "display",
120 "displaystyle", "encoding", "fence", "form", "largeop", "linebreak",
121 "linethickness", "lspace", "mathcolor", "mathsize", "mathvariant", "minsize",
122 "movablelimits", "notation", "rowalign", "rspace", "rowspacing", "rowspan",
123 "scriptlevel", "stretchy", "symmetric", "title", "voffset", "width",
124 }
125 mathElements := []string{
126 "annotation", "math", "menclose", "merror", "mfrac", "mi", "mmultiscripts",
127 "mn", "mo", "mover", "mpadded", "mprescripts", "mroot", "mrow", "mspace",
128 "msqrt", "mstyle", "msub", "msubsup", "msup", "mtable", "mtd", "mtext",
129 "mtr", "munder", "munderover", "semantics",
130 }
131 policy.AllowNoAttrs().OnElements(mathElements...)
132 policy.AllowAttrs(mathAttrs...).OnElements(mathElements...)
133
134 // goldmark-callout
135 policy.AllowAttrs("data-callout").OnElements("details")
136
137 return policy
138}
139
140func buildDescriptionPolicy() *bluemonday.Policy {
141 policy := bluemonday.NewPolicy()
142 policy.AllowStandardURLs()
143
144 // allow italics and bold.
145 policy.AllowElements("i", "b", "em", "strong")
146
147 // allow code.
148 policy.AllowElements("code")
149
150 // allow links
151 policy.AllowAttrs("href", "target", "rel").OnElements("a")
152
153 return policy
154}
155
156func buildLogsPolicy() *bluemonday.Policy {
157 policy := bluemonday.NewPolicy()
158
159 policy.AllowElements("p", "span")
160
161 // allow italics and bold
162 policy.AllowElements("i", "b", "em", "strong")
163
164 // allow fg/bg classes from terminal-to-html
165 policy.AllowAttrs("class").Matching(regexp.MustCompile(`term-*`)).OnElements("span")
166
167 return policy
168}