Monorepo for Tangled tangled.org

improve code blocks in markdown (syntax highlighting and copy-to-clipboard button) #145

closed opened by gwen.works targeting master from gwen.works/core: copycodeblocks

right now the button's text gets added without the around it. can't figure out why. am i missing sth obvious ?

Labels

None yet.

assignee

None yet.

Participants 3
AT URI
at://did:plc:dpng6jhpwdz7yifx722rh7w6/sh.tangled.repo.pull/3lppegz7mnj22
+205 -13
Diff #3
+2 -1
go.mod
··· 29 29 github.com/resend/resend-go/v2 v2.15.0 30 30 github.com/sethvargo/go-envconfig v1.1.0 31 31 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 32 - github.com/yuin/goldmark v1.4.13 32 + github.com/yuin/goldmark v1.4.15 33 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 33 34 golang.org/x/net v0.39.0 34 35 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 35 36 )
+8 -1
go.sum
··· 9 9 github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= 10 10 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 11 11 github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 12 + github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= 12 13 github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= 13 14 github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= 15 + github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 14 16 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 15 17 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 16 18 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= ··· 55 57 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 56 58 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 57 59 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 60 + github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 61 + github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 58 62 github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 59 63 github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 60 64 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= ··· 279 283 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 280 284 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 281 285 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 282 - github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= 283 286 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 287 + github.com/yuin/goldmark v1.4.15 h1:CFa84T0goNn/UIXYS+dmjjVxMyTAvpOmzld40N/nfK0= 288 + github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 289 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 290 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 284 291 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 285 292 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 286 293 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
+183 -6
appview/pages/markup/markdown.go
··· 9 9 "path" 10 10 "strings" 11 11 12 + "github.com/alecthomas/chroma/v2/styles" 12 13 "github.com/microcosm-cc/bluemonday" 13 14 "github.com/yuin/goldmark" 15 + "github.com/yuin/goldmark-highlighting/v2" 14 16 "github.com/yuin/goldmark/ast" 15 17 "github.com/yuin/goldmark/extension" 16 18 "github.com/yuin/goldmark/parser" 17 19 20 + "github.com/yuin/goldmark/text" 21 + "github.com/yuin/goldmark/util" 22 + htmlparse "golang.org/x/net/html" 23 + "golang.org/x/net/html/atom" 18 24 25 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 26 + ) 19 27 20 28 21 29 ··· 34 42 35 43 36 44 45 + RendererType RendererType 46 + } 37 47 48 + // RenderMarkdown renders the given markdown source into sanitized HTML. sanitizer is used to sanitize the HTML output. 49 + func (rctx *RenderContext) RenderMarkdown(source string, sanitizer func(string) string) string { 50 + md := goldmark.New( 51 + goldmark.WithExtensions(extension.GFM, 52 + highlighting.NewHighlighting( 53 + highlighting.WithCustomStyle( 54 + styles.Get("catppuccin-latte"), 55 + ), 56 + ), 57 + ), 58 + goldmark.WithParserOptions( 59 + parser.WithAutoHeadingID(), 60 + ), 38 61 39 62 40 63 ··· 42 65 43 66 44 67 45 - func (rctx *RenderContext) RenderMarkdown(source string) string { 46 - md := goldmark.New( 47 - goldmark.WithExtensions(extension.GFM), 48 - goldmark.WithParserOptions( 49 - parser.WithAutoHeadingID(), 50 - ), 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + return source 77 + } 78 + 79 + sanitizedHtml := sanitizer(buf.String()) 80 + 81 + var processed strings.Builder 82 + if err := postProcessSanitizedHtml(rctx, strings.NewReader(sanitizedHtml), &processed); err != nil { 83 + return source 84 + } 85 + 86 + return processed.String() 87 + } 88 + 89 + // postProcessSanitizedHtml processes the HTML output from the markdown renderer. 90 + // WARNING: Do not insert raw HTML from user-controlled input. Sanitization already happened beforehand at this point. 91 + func postProcessSanitizedHtml(ctx *RenderContext, input io.Reader, output io.Writer) error { 92 + node, err := htmlparse.Parse(io.MultiReader( 93 + strings.NewReader("<html><body>"), 94 + input, 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 + 120 + 121 + 122 + 123 + 124 + 125 + 126 + 127 + 128 + 129 + 130 + 131 + 132 + 133 + return nil 134 + } 135 + 136 + // visitNode is called on every node of a SANITIZED html document. 137 + // WARNING: Do not insert raw HTML from user-controlled input. Sanitization already happened beforehand at this point. 138 + func visitNode(ctx *RenderContext, node *htmlparse.Node) { 139 + switch node.Type { 140 + case htmlparse.ElementNode: 141 + 142 + 143 + 144 + 145 + 146 + 147 + 148 + 149 + 150 + 151 + 152 + 153 + 154 + } 155 + } 156 + 157 + if node.Data == "pre" { 158 + // TODO only show when :hover or :focus on the pre element 159 + button := &htmlparse.Node{ 160 + Type: htmlparse.ElementNode, 161 + DataAtom: atom.Button, 162 + Data: "button", 163 + Attr: []htmlparse.Attribute{ 164 + { 165 + Key: "class", 166 + Val: "absolute top-2 right-2 btn", 167 + }, 168 + { 169 + Key: "style", 170 + // FIXME .#watch-tailwind doesnt seem to catch top-2 and right-2, probably cuz it's not used anywhere inside of templates/ ? 171 + Val: "top: 0.5rem; right: 0.5rem;", 172 + }, 173 + { 174 + Key: "onclick", 175 + Val: ` 176 + navigator.clipboard.writeText(this.closest('pre').querySelector('code').innerText); 177 + this.innerText = 'Copied!'; 178 + setTimeout(() => { this.innerText = 'Copy' }, 1500); 179 + `, 180 + }, 181 + // FIXME: onload does not fire :/ 182 + // { 183 + // Key: "onload", 184 + // Val: "this.removeAttribute('aria-hidden')", 185 + // }, 186 + // { 187 + // Key: "aria-hidden", 188 + // Val: "true", 189 + // }, 190 + { 191 + Key: "title", 192 + Val: "Copy to clipboard", 193 + }, 194 + }, 195 + } 196 + 197 + // TODO 198 + // if copyIcon, err := icons.IconNode("copy", "h-4", "w-4"); err != nil { 199 + // button.AppendChild(copyIcon) 200 + // } else { 201 + button.AppendChild(&htmlparse.Node{ 202 + Type: htmlparse.TextNode, 203 + Data: "Copy", 204 + }) 205 + 206 + var classWasSetOnNode bool 207 + for i, attr := range node.Attr { 208 + if attr.Key == "class" { 209 + node.Attr[i].Val += " relative" 210 + classWasSetOnNode = true 211 + break 212 + } 213 + } 214 + 215 + if !classWasSetOnNode { 216 + node.Attr = append(node.Attr, htmlparse.Attribute{ 217 + Key: "class", 218 + Val: "relative", 219 + }) 220 + } 221 + 222 + node.AppendChild(button) 223 + } 224 + 225 + for n := node.FirstChild; n != nil; n = n.NextSibling { 226 + visitNode(ctx, n) 227 + }
+3 -1
appview/pages/funcmap.go
··· 145 145 }, 146 146 "markdown": func(text string) template.HTML { 147 147 rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 148 - return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text))) 148 + return template.HTML( 149 + rctx.RenderMarkdown(text, bluemonday.UGCPolicy().Sanitize), 150 + ) 149 151 }, 150 152 "isNil": func(t any) bool { 151 153 // returns false for other "zero" values
+4 -4
appview/pages/pages.go
··· 432 432 ext := filepath.Ext(params.ReadmeFileName) 433 433 switch ext { 434 434 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 435 - htmlString = p.rctx.RenderMarkdown(params.Readme) 435 + htmlString = p.rctx.RenderMarkdown(params.Readme, p.rctx.Sanitize) 436 436 params.Raw = false 437 - params.HTMLReadme = template.HTML(p.rctx.Sanitize(htmlString)) 437 + params.HTMLReadme = template.HTML(htmlString) 438 438 default: 439 439 htmlString = string(params.Readme) 440 440 params.Raw = true ··· 564 564 case markup.FormatMarkdown: 565 565 p.rctx.RepoInfo = params.RepoInfo 566 566 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 567 - htmlString := p.rctx.RenderMarkdown(params.Contents) 568 - params.RenderedContents = template.HTML(p.rctx.Sanitize(htmlString)) 567 + htmlString := p.rctx.RenderMarkdown(params.Contents, p.rctx.Sanitize) 568 + params.RenderedContents = template.HTML(htmlString) 569 569 } 570 570 } 571 571
+5
input.css
··· 104 104 } 105 105 } 106 106 107 + /* Hidden elements */ 108 + [aria-hidden="true"] { 109 + display: none ; 110 + } 111 + 107 112 /* Background */ 108 113 .bg { 109 114 color: #4c4f69;

History

6 rounds 10 comments
sign up or login to add to the discussion
6 commits
expand
deps: add goldmark-highlighting
appview: markup: highlight rendered markdown code blocks
appview: pages: post-process rendered markdown after sanitization
appview: input.css: hide [aria-hidden="true"] elements
(wip)appview: markup: add copy button to markdown code blocks
(to-revert)appview: markup: work around the fact that [onload] is cringe
expand 7 comments

is this still being worked on? (i see some wip commits)

(no rush whatsoever, just checking in to see if i can help in any way!)

yeah sorry for the whole-month silence lol, its mostly tailwind shenanigans and i had to work around it and it's quite ugly

there was also the thing with sanitizing user html later, ill try to finish it over the weekend, idk how much conflicted i have become with main lol (long running feature branches = hell)

FYI there is another person working on just the syntax highlighting side of things, see Discord: https://discord.com/channels/1361963801993285692/1392475067962753116.

closing this now, syntax highlighting in md blocks has been added.

closed without merging
6 commits
expand
deps: add goldmark-highlighting
appview: markup: highlight rendered markdown code blocks
appview: pages: post-process rendered markdown after sanitization
appview: input.css: hide [aria-hidden="true"] elements
(wip)appview: markup: add copy button to markdown code blocks
(to-revert)appview: markup: work around the fact that [onload] is cringe
expand 1 comment

round #4: sign-off commits and rebase atop main

6 commits
expand
deps: add goldmark-highlighting
appview: markup: highlight rendered markdown code blocks
appview: pages: post-process rendered markdown after sanitization
appview: input.css: hide [aria-hidden="true"] elements
(wip)appview: markup: add copy button to markdown code blocks
(to-revert)appview: markup: work around the fact that [onload] is cringe
expand 0 comments
5 commits
expand
deps: add goldmark-highlighting
appview: markup: highlight rendered markdown code blocks
appview: pages: post-process rendered markdown after sanitization
appview: input.css: hide [aria-hidden="true"] elements
(wip)appview: markup: add copy button to markdown code blocks
expand 1 comment

round #2 is just my dumb ass making commit messages conform with the contributing guide

5 commits
expand
deps: add goldmark-highlighting
appview: highlight rendered markdown code blocks
appview: post-process rendered markdown after sanitization
appview: hide [aria-hidden="true"] elements
(wip)appview: add copy button
expand 0 comments
3 commits
expand
deps: add goldmark-highlighting
appview: highlight rendered markdown code blocks
(wip)appview: add copy button
expand 1 comment

i uhhh meant "without the <button> around it", cant edit pr body