From a4925feae72d474bf9808ae57c5ee21abf47b2f8 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Sat, 4 Oct 2025 09:27:41 +0300 Subject: [PATCH] appview/pages/markup: add support for callouts Change-Id: pyllqzrprrnyyspumlyrupuopomupuxw Signed-off-by: Anirudh Oppiliappan --- appview/pages/funcmap.go | 8 +-- appview/pages/markup/markdown.go | 7 ++- appview/pages/markup/sanitizer.go | 3 ++ appview/pages/pages.go | 7 +-- go.mod | 1 + go.sum | 2 + input.css | 83 ++++++++++++++++++++++++++----- 7 files changed, 91 insertions(+), 20 deletions(-) diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index 0ad47269..be646568 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -265,14 +265,14 @@ func (p *Pages) funcMap() template.FuncMap { return nil }, "i": func(name string, classes ...string) template.HTML { - data, err := icon(name, classes) + data, err := p.icon(name, classes) if err != nil { log.Printf("icon %s does not exist", name) - data, _ = icon("airplay", classes) + data, _ = p.icon("airplay", classes) } return template.HTML(data) }, - "cssContentHash": CssContentHash, + "cssContentHash": p.CssContentHash, "fileTree": filetree.FileTree, "pathEscape": func(s string) string { return url.PathEscape(s) @@ -325,7 +325,7 @@ func (p *Pages) avatarUri(handle, size string) string { return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) } -func icon(name string, classes []string) (template.HTML, error) { +func (p *Pages) icon(name string, classes []string) (template.HTML, error) { iconPath := filepath.Join("static", "icons", name) if filepath.Ext(name) == "" { diff --git a/appview/pages/markup/markdown.go b/appview/pages/markup/markdown.go index 3cbe3659..3f561d15 100644 --- a/appview/pages/markup/markdown.go +++ b/appview/pages/markup/markdown.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "io" + "io/fs" "net/url" "path" "strings" @@ -20,6 +21,7 @@ import ( "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" + callout "gitlab.com/staticnoise/goldmark-callout" htmlparse "golang.org/x/net/html" "tangled.org/core/api/tangled" @@ -45,6 +47,7 @@ type RenderContext struct { IsDev bool RendererType RendererType Sanitizer Sanitizer + Files fs.FS } func (rctx *RenderContext) RenderMarkdown(source string) string { @@ -62,6 +65,7 @@ func (rctx *RenderContext) RenderMarkdown(source string) string { extension.WithFootnoteIDPrefix([]byte("footnote")), ), treeblood.MathML(), + callout.CalloutExtention, ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), @@ -140,7 +144,8 @@ func postProcess(ctx *RenderContext, input io.Reader, output io.Writer) error { func visitNode(ctx *RenderContext, node *htmlparse.Node) { switch node.Type { case htmlparse.ElementNode: - if node.Data == "img" || node.Data == "source" { + switch node.Data { + case "img", "source": for i, attr := range node.Attr { if attr.Key != "src" { continue diff --git a/appview/pages/markup/sanitizer.go b/appview/pages/markup/sanitizer.go index 9a2f239e..a2855208 100644 --- a/appview/pages/markup/sanitizer.go +++ b/appview/pages/markup/sanitizer.go @@ -114,6 +114,9 @@ func defaultPolicy() *bluemonday.Policy { policy.AllowNoAttrs().OnElements(mathElements...) policy.AllowAttrs(mathAttrs...).OnElements(mathElements...) + // goldmark-callout + policy.AllowAttrs("data-callout").OnElements("details") + return policy } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index c0d4a036..58d50980 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -61,6 +61,7 @@ func NewPages(config *config.Config, res *idresolver.Resolver) *Pages { CamoUrl: config.Camo.Host, CamoSecret: config.Camo.SharedSecret, Sanitizer: markup.NewSanitizer(), + Files: Files, } p := &Pages{ @@ -1474,7 +1475,7 @@ func (p *Pages) Static() http.Handler { return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) } - sub, err := fs.Sub(Files, "static") + sub, err := fs.Sub(p.embedFS, "static") if err != nil { p.logger.Error("no static dir found? that's crazy", "err", err) panic(err) @@ -1497,8 +1498,8 @@ func Cache(h http.Handler) http.Handler { }) } -func CssContentHash() string { - cssFile, err := Files.Open("static/tw.css") +func (p *Pages) CssContentHash() string { + cssFile, err := p.embedFS.Open("static/tw.css") if err != nil { slog.Debug("Error opening CSS file", "err", err) return "" diff --git a/go.mod b/go.mod index a5e519bc..7f716c1d 100644 --- a/go.mod +++ b/go.mod @@ -157,6 +157,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wyatt915/treeblood v0.1.15 // indirect + gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab // indirect gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index 79435e00..1d2ee0e9 100644 --- a/go.sum +++ b/go.sum @@ -440,6 +440,8 @@ github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A= +gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab/go.mod h1:SPu13/NPe1kMrbGoJldQwqtpNhXsmIuHCfm/aaGjU0c= gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= diff --git a/input.css b/input.css index 25bcef1a..c04d53c2 100644 --- a/input.css +++ b/input.css @@ -134,39 +134,40 @@ } .prose hr { - @apply my-2; + @apply my-2; } .prose li:has(input) { - @apply list-none; + @apply list-none; } .prose ul:has(input) { - @apply pl-2; + @apply pl-2; } .prose .heading .anchor { - @apply no-underline mx-2 opacity-0; + @apply no-underline mx-2 opacity-0; } .prose .heading:hover .anchor { - @apply opacity-70; + @apply opacity-70; } .prose .heading .anchor:hover { - @apply opacity-70; + @apply opacity-70; } .prose a.footnote-backref { - @apply no-underline; + @apply no-underline; } .prose li { - @apply my-0 py-0; + @apply my-0 py-0; } - .prose ul, .prose ol { - @apply my-1 py-0; + .prose ul, + .prose ol { + @apply my-1 py-0; } .prose img { @@ -176,12 +177,70 @@ } .prose input { - @apply inline-block my-0 mb-1 mx-1; + @apply inline-block my-0 mb-1 mx-1; } .prose input[type="checkbox"] { @apply disabled:accent-blue-500 checked:accent-blue-500 disabled:checked:accent-blue-500; } + + /* Base callout */ + details[data-callout] { + @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4; + } + + details[data-callout] > summary { + @apply font-bold cursor-pointer mb-1; + } + + details[data-callout] > .callout-content { + @apply text-sm leading-snug; + } + + /* Note (blue) */ + details[data-callout="note" i] { + @apply border-blue-400 dark:border-blue-500; + } + details[data-callout="note" i] > summary { + @apply text-blue-700 dark:text-blue-400; + } + + /* Important (purple) */ + details[data-callout="important" i] { + @apply border-purple-400 dark:border-purple-500; + } + details[data-callout="important" i] > summary { + @apply text-purple-700 dark:text-purple-400; + } + + /* Warning (yellow) */ + details[data-callout="warning" i] { + @apply border-yellow-400 dark:border-yellow-500; + } + details[data-callout="warning" i] > summary { + @apply text-yellow-700 dark:text-yellow-400; + } + + /* Caution (red) */ + details[data-callout="caution" i] { + @apply border-red-400 dark:border-red-500; + } + details[data-callout="caution" i] > summary { + @apply text-red-700 dark:text-red-400; + } + + /* Tip (green) */ + details[data-callout="tip" i] { + @apply border-green-400 dark:border-green-500; + } + details[data-callout="tip" i] > summary { + @apply text-green-700 dark:text-green-400; + } + + /* Optional: hide the disclosure arrow like GitHub */ + details[data-callout] > summary::-webkit-details-marker { + display: none; + } } @layer utilities { .error { @@ -228,7 +287,7 @@ } /* LineHighlight */ .chroma .hl { - @apply bg-amber-400/30 dark:bg-amber-500/20; + @apply bg-amber-400/30 dark:bg-amber-500/20; } /* LineNumbersTable */ -- 2.43.0