Monorepo for Tangled — https://tangled.org

wip goldmark callouts

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

anirudh.fi a84598a8 5ecd54b3

verified
+190 -20
+4 -4
appview/pages/funcmap.go
··· 265 265 return nil 266 266 }, 267 267 "i": func(name string, classes ...string) template.HTML { 268 - data, err := icon(name, classes) 268 + data, err := p.icon(name, classes) 269 269 if err != nil { 270 270 log.Printf("icon %s does not exist", name) 271 - data, _ = icon("airplay", classes) 271 + data, _ = p.icon("airplay", classes) 272 272 } 273 273 return template.HTML(data) 274 274 }, 275 - "cssContentHash": CssContentHash, 275 + "cssContentHash": p.CssContentHash, 276 276 "fileTree": filetree.FileTree, 277 277 "pathEscape": func(s string) string { 278 278 return url.PathEscape(s) ··· 325 325 return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) 326 326 } 327 327 328 - func icon(name string, classes []string) (template.HTML, error) { 328 + func (p *Pages) icon(name string, classes []string) (template.HTML, error) { 329 329 iconPath := filepath.Join("static", "icons", name) 330 330 331 331 if filepath.Ext(name) == "" {
+97
appview/pages/markup/callouts.go
··· 1 + package markup 2 + 3 + import ( 4 + "embed" 5 + "fmt" 6 + "path/filepath" 7 + "strings" 8 + 9 + "golang.org/x/net/html" 10 + htmlparse "golang.org/x/net/html" 11 + ) 12 + 13 + func (rctx *RenderContext) iconNode(name string, classes []string) (*html.Node, error) { 14 + iconPath := filepath.Join("static", "icons", name) 15 + 16 + if filepath.Ext(name) == "" { 17 + iconPath += ".svg" 18 + } 19 + 20 + data, err := rctx.Files.(embed.FS).ReadFile(iconPath) 21 + if err != nil { 22 + return nil, fmt.Errorf("icon %s not found: %w", name, err) 23 + } 24 + 25 + // parse svg into html node tree 26 + root, err := html.Parse(strings.NewReader(string(data))) 27 + if err != nil { 28 + return nil, fmt.Errorf("failed to parse SVG for %s: %w", name, err) 29 + } 30 + 31 + var svgNode *html.Node 32 + var findSVG func(*html.Node) 33 + findSVG = func(n *html.Node) { 34 + if n.Type == html.ElementNode && n.Data == "svg" { 35 + svgNode = n 36 + return 37 + } 38 + for c := n.FirstChild; c != nil; c = c.NextSibling { 39 + findSVG(c) 40 + } 41 + } 42 + findSVG(root) 43 + 44 + if svgNode == nil { 45 + return nil, fmt.Errorf("no <svg> element found in %s", name) 46 + } 47 + 48 + classVal := strings.Join(classes, " ") 49 + hasClass := false 50 + for i, a := range svgNode.Attr { 51 + if a.Key == "class" { 52 + svgNode.Attr[i].Val = strings.TrimSpace(a.Val + " " + classVal) 53 + hasClass = true 54 + break 55 + } 56 + } 57 + if !hasClass { 58 + svgNode.Attr = append(svgNode.Attr, html.Attribute{Key: "class", Val: classVal}) 59 + } 60 + 61 + return svgNode, nil 62 + } 63 + 64 + // inject svg icon into callout 65 + func (rctx *RenderContext) injectSVG(node *html.Node) { 66 + var calloutIconMap = map[string]string{ 67 + "note": "info", 68 + "tip": "lightbulb", 69 + "important": "message-square-warning", 70 + "warning": "triangle-alert", 71 + "caution": "octagon-alert", 72 + } 73 + var calloutType string 74 + for _, attr := range node.Attr { 75 + if attr.Key == "data-callout" { 76 + calloutType = attr.Val 77 + break 78 + } 79 + } 80 + 81 + if calloutType != "" { 82 + for c := node.FirstChild; c != nil; c = c.NextSibling { 83 + if c.Type == htmlparse.ElementNode && c.Data == "summary" { 84 + iconName := fmt.Sprintf("%s.svg", calloutIconMap[calloutType]) 85 + inode, err := rctx.iconNode(iconName, []string{"inline-block", "size-4", "mr-2"}) 86 + if err != nil { 87 + break 88 + } 89 + 90 + // insert icon node as the first child inside <summary> 91 + inode.NextSibling = c.FirstChild 92 + c.FirstChild = inode 93 + break 94 + } 95 + } 96 + } 97 + }
+8 -1
appview/pages/markup/markdown.go
··· 5 5 "bytes" 6 6 "fmt" 7 7 "io" 8 + "io/fs" 8 9 "net/url" 9 10 "path" 10 11 "strings" ··· 20 21 "github.com/yuin/goldmark/renderer/html" 21 22 "github.com/yuin/goldmark/text" 22 23 "github.com/yuin/goldmark/util" 24 + callout "gitlab.com/staticnoise/goldmark-callout" 23 25 htmlparse "golang.org/x/net/html" 24 26 25 27 "tangled.org/core/api/tangled" ··· 45 47 IsDev bool 46 48 RendererType RendererType 47 49 Sanitizer Sanitizer 50 + Files fs.FS 48 51 } 49 52 50 53 func (rctx *RenderContext) RenderMarkdown(source string) string { ··· 62 65 extension.WithFootnoteIDPrefix([]byte("footnote")), 63 66 ), 64 67 treeblood.MathML(), 68 + callout.CalloutExtention, 65 69 ), 66 70 goldmark.WithParserOptions( 67 71 parser.WithAutoHeadingID(), ··· 140 144 func visitNode(ctx *RenderContext, node *htmlparse.Node) { 141 145 switch node.Type { 142 146 case htmlparse.ElementNode: 143 - if node.Data == "img" || node.Data == "source" { 147 + switch node.Data { 148 + case "img", "source": 144 149 for i, attr := range node.Attr { 145 150 if attr.Key != "src" { 146 151 continue ··· 154 159 node.Attr[i] = attr 155 160 } 156 161 } 162 + case "details": 163 + ctx.injectSVG(node) 157 164 } 158 165 159 166 for n := node.FirstChild; n != nil; n = n.NextSibling {
+3
appview/pages/markup/sanitizer.go
··· 114 114 policy.AllowNoAttrs().OnElements(mathElements...) 115 115 policy.AllowAttrs(mathAttrs...).OnElements(mathElements...) 116 116 117 + // goldmark-callout 118 + policy.AllowAttrs("data-callout").OnElements("details") 119 + 117 120 return policy 118 121 } 119 122
+4 -3
appview/pages/pages.go
··· 61 61 CamoUrl: config.Camo.Host, 62 62 CamoSecret: config.Camo.SharedSecret, 63 63 Sanitizer: markup.NewSanitizer(), 64 + Files: Files, 64 65 } 65 66 66 67 p := &Pages{ ··· 1474 1475 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1475 1476 } 1476 1477 1477 - sub, err := fs.Sub(Files, "static") 1478 + sub, err := fs.Sub(p.embedFS, "static") 1478 1479 if err != nil { 1479 1480 p.logger.Error("no static dir found? that's crazy", "err", err) 1480 1481 panic(err) ··· 1497 1498 }) 1498 1499 } 1499 1500 1500 - func CssContentHash() string { 1501 - cssFile, err := Files.Open("static/tw.css") 1501 + func (p *Pages) CssContentHash() string { 1502 + cssFile, err := p.embedFS.Open("static/tw.css") 1502 1503 if err != nil { 1503 1504 slog.Debug("Error opening CSS file", "err", err) 1504 1505 return ""
+1
go.mod
··· 157 157 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 158 158 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 159 159 github.com/wyatt915/treeblood v0.1.15 // indirect 160 + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect 160 161 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 161 162 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 162 163 go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+2
go.sum
··· 440 440 github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 441 441 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 442 442 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 443 + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A= 444 + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c= 443 445 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 444 446 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 445 447 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
+71 -12
input.css
··· 134 134 } 135 135 136 136 .prose hr { 137 - @apply my-2; 137 + @apply my-2; 138 138 } 139 139 140 140 .prose li:has(input) { 141 - @apply list-none; 141 + @apply list-none; 142 142 } 143 143 144 144 .prose ul:has(input) { 145 - @apply pl-2; 145 + @apply pl-2; 146 146 } 147 147 148 148 .prose .heading .anchor { 149 - @apply no-underline mx-2 opacity-0; 149 + @apply no-underline mx-2 opacity-0; 150 150 } 151 151 152 152 .prose .heading:hover .anchor { 153 - @apply opacity-70; 153 + @apply opacity-70; 154 154 } 155 155 156 156 .prose .heading .anchor:hover { 157 - @apply opacity-70; 157 + @apply opacity-70; 158 158 } 159 159 160 160 .prose a.footnote-backref { 161 - @apply no-underline; 161 + @apply no-underline; 162 162 } 163 163 164 164 .prose li { 165 - @apply my-0 py-0; 165 + @apply my-0 py-0; 166 166 } 167 167 168 - .prose ul, .prose ol { 169 - @apply my-1 py-0; 168 + .prose ul, 169 + .prose ol { 170 + @apply my-1 py-0; 170 171 } 171 172 172 173 .prose img { ··· 176 177 } 177 178 178 179 .prose input { 179 - @apply inline-block my-0 mb-1 mx-1; 180 + @apply inline-block my-0 mb-1 mx-1; 180 181 } 181 182 182 183 .prose input[type="checkbox"] { 183 184 @apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500; 184 185 } 186 + 187 + /* Base callout */ 188 + details[data-callout] { 189 + @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4; 190 + } 191 + 192 + details[data-callout] > summary { 193 + @apply font-bold cursor-pointer mb-1; 194 + } 195 + 196 + details[data-callout] > .callout-content { 197 + @apply text-sm leading-snug; 198 + } 199 + 200 + /* Note (blue) */ 201 + details[data-callout="note"] { 202 + @apply border-blue-400 dark:border-blue-500; 203 + } 204 + details[data-callout="note"] > summary { 205 + @apply text-blue-700 dark:text-blue-400; 206 + } 207 + 208 + /* Important (purple) */ 209 + details[data-callout="important"] { 210 + @apply border-purple-400 dark:border-purple-500; 211 + } 212 + details[data-callout="important"] > summary { 213 + @apply text-purple-700 dark:text-purple-400; 214 + } 215 + 216 + /* Warning (yellow) */ 217 + details[data-callout="warning"] { 218 + @apply border-yellow-400 dark:border-yellow-500; 219 + } 220 + details[data-callout="warning"] > summary { 221 + @apply text-yellow-700 dark:text-yellow-400; 222 + } 223 + 224 + /* Caution (red) */ 225 + details[data-callout="caution"] { 226 + @apply border-red-400 dark:border-red-500; 227 + } 228 + details[data-callout="caution"] > summary { 229 + @apply text-red-700 dark:text-red-400; 230 + } 231 + 232 + /* Tip (green) */ 233 + details[data-callout="tip"] { 234 + @apply border-green-400 dark:border-green-500; 235 + } 236 + details[data-callout="tip"] > summary { 237 + @apply text-green-700 dark:text-green-400; 238 + } 239 + 240 + /* Optional: hide the disclosure arrow like GitHub */ 241 + details[data-callout] > summary::-webkit-details-marker { 242 + display: none; 243 + } 185 244 } 186 245 @layer utilities { 187 246 .error { ··· 228 287 } 229 288 /* LineHighlight */ 230 289 .chroma .hl { 231 - @apply bg-amber-400/30 dark:bg-amber-500/20; 290 + @apply bg-amber-400/30 dark:bg-amber-500/20; 232 291 } 233 292 234 293 /* LineNumbersTable */