Commit af7479c3da
Verified · cmc ci/build: success
e2e/design_test.go +4 −2
| @@ -28,7 +28,8 @@ func TestReadmeRelativeLinks(t *testing.T) { | ||
| 28 | 28 | "# site\n\n[guide](docs/guide.md) and [export](docs/paper.html) and "+ |
| 29 | 29 | "[abs](https://example.org/x) here\n\n\n"+ |
| 30 | 30 | "\n\n"+ |
| 31 | "| flag | effect |\n|------|--------|\n| `-v` | verbose |\n"), 0o644) | |
| 31 | "| flag | effect |\n|------|--------|\n| `-v` | verbose |\n\n"+ | |
| 32 | "```go\nfunc main() {}\n```\n"), 0o644) | |
| 32 | 33 | os.WriteFile(filepath.Join(dir, "docs", "guide.md"), []byte("# guide\n"), 0o644) |
| 33 | 34 | os.WriteFile(filepath.Join(dir, "docs", "paper.org"), []byte("* paper\n"), 0o644) |
| 34 | 35 | os.WriteFile(filepath.Join(dir, "img", "logo.png"), []byte{0x89, 0x50}, 0o644) |
| @@ -48,6 +49,7 @@ func TestReadmeRelativeLinks(t *testing.T) { | ||
| 48 | 49 | `src="https://example.org/pic.png"`, // remote image untouched |
| 49 | 50 | `href="https://example.org/x"`, // absolute untouched |
| 50 | 51 | "<table>", "<td>verbose</td>", // GFM table renders |
| 52 | `<span class="kd">func</span>`, // fenced code highlighted via classes | |
| 51 | 53 | `href="/alice/site/blob/main/README.md">README.md</a>`, // clickable card header |
| 52 | 54 | `<th>name</th>`, // file table column headers |
| 53 | 55 | } { |
| @@ -108,7 +110,7 @@ func TestReadmeRelativeLinks(t *testing.T) { | ||
| 108 | 110 | t.Errorf("highlighting not class-based:\n%.2000s", body) |
| 109 | 111 | } |
| 110 | 112 | if _, css := inst.get(t, "/static/style.css"); strings.Count(css, "/* Background */") < 2 || |
| 111 | !strings.Contains(css, ".chroma, .bg { background: var(--code-bg)") { | |
| 113 | !strings.Contains(css, ".chroma, .bg { background: transparent") { | |
| 112 | 114 | t.Error("stylesheet missing dual syntax palettes") |
| 113 | 115 | } |
| 114 | 116 | // Explore rows carry topics, license, and updated date. |
go.mod +2 −1
| @@ -10,6 +10,8 @@ require ( | ||
| 10 | 10 | github.com/niklasfasching/go-org v1.9.1 |
| 11 | 11 | github.com/spf13/cobra v1.10.2 |
| 12 | 12 | github.com/yuin/goldmark v1.8.5 |
| 13 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc | |
| 14 | go.yaml.in/yaml/v3 v3.0.4 | |
| 13 | 15 | golang.org/x/crypto v0.55.0 |
| 14 | 16 | golang.org/x/net v0.57.0 |
| 15 | 17 | golang.org/x/term v0.45.0 |
| @@ -30,7 +32,6 @@ require ( | ||
| 30 | 32 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect |
| 31 | 33 | github.com/russross/blackfriday/v2 v2.1.0 // indirect |
| 32 | 34 | github.com/spf13/pflag v1.0.9 // indirect |
| 33 | go.yaml.in/yaml/v3 v3.0.4 // indirect | |
| 34 | 35 | golang.org/x/sys v0.47.0 // indirect |
| 35 | 36 | golang.org/x/text v0.41.0 // indirect |
| 36 | 37 | modernc.org/libc v1.74.4 // indirect |
go.sum +12
| @@ -4,8 +4,10 @@ github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67 | ||
| 4 | 4 | github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= |
| 5 | 5 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= |
| 6 | 6 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= |
| 7 | github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= | |
| 7 | 8 | github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= |
| 8 | 9 | github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= |
| 10 | github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= | |
| 9 | 11 | github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= |
| 10 | 12 | github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= |
| 11 | 13 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= |
| @@ -14,6 +16,10 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg | ||
| 14 | 16 | github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= |
| 15 | 17 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= |
| 16 | 18 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= |
| 19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
| 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |
| 21 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= | |
| 22 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= | |
| 17 | 23 | github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= |
| 18 | 24 | github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= |
| 19 | 25 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= |
| @@ -48,8 +54,13 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= | ||
| 48 | 54 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= |
| 49 | 55 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= |
| 50 | 56 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= |
| 57 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | |
| 58 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | |
| 59 | github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |
| 51 | 60 | github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= |
| 52 | 61 | github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= |
| 62 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= | |
| 63 | github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= | |
| 53 | 64 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= |
| 54 | 65 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= |
| 55 | 66 | golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= |
| @@ -70,6 +81,7 @@ golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= | ||
| 70 | 81 | golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= |
| 71 | 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
| 72 | 83 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
| 84 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |
| 73 | 85 | modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= |
| 74 | 86 | modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= |
| 75 | 87 | modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= |
internal/httpd/web.go +45 −7
| @@ -23,6 +23,7 @@ import ( | ||
| 23 | 23 | "github.com/microcosm-cc/bluemonday" |
| 24 | 24 | "github.com/niklasfasching/go-org/org" |
| 25 | 25 | "github.com/yuin/goldmark" |
| 26 | highlighting "github.com/yuin/goldmark-highlighting/v2" | |
| 26 | 27 | "github.com/yuin/goldmark/extension" |
| 27 | 28 | |
| 28 | 29 | "gitbay.org/gitbay/internal/autolink" |
| @@ -765,7 +766,7 @@ var chromaCSS = func() []byte { | ||
| 765 | 766 | chromaFormatter.WriteCSS(&buf, styles.Get("friendly")) |
| 766 | 767 | buf.WriteString("\n@media (prefers-color-scheme: dark) {\n") |
| 767 | 768 | chromaFormatter.WriteCSS(&buf, styles.Get("github-dark")) |
| 768 | buf.WriteString("}\n.chroma, .bg { background: var(--code-bg) !important; }\n") | |
| 769 | buf.WriteString("}\n.chroma, .bg { background: transparent !important; }\n") | |
| 769 | 770 | return buf.Bytes() |
| 770 | 771 | }() |
| 771 | 772 | |
| @@ -828,8 +829,30 @@ func pickReadme(entries []gitutil.TreeEntry) string { | ||
| 828 | 829 | } |
| 829 | 830 | |
| 830 | 831 | // markdown is the shared renderer: GFM (tables, strikethrough, autolinks, |
| 831 | // task lists) on top of CommonMark. Raw HTML is still dropped. | |
| 832 | var markdown = goldmark.New(goldmark.WithExtensions(extension.GFM)) | |
| 832 | // task lists) on top of CommonMark, with class-based fence highlighting | |
| 833 | // (the palette lives in the stylesheet, per scheme). Raw HTML is still | |
| 834 | // dropped. | |
| 835 | var markdown = goldmark.New(goldmark.WithExtensions(extension.GFM, | |
| 836 | highlighting.NewHighlighting(highlighting.WithFormatOptions(html.WithClasses(true))))) | |
| 837 | ||
| 838 | // fenceHighlight renders one code block with chroma classes, for org and | |
| 839 | // anything else outside goldmark. Unknown languages fall back to plain. | |
| 840 | func fenceHighlight(source, lang string) string { | |
| 841 | lexer := lexers.Get(lang) | |
| 842 | if lexer == nil { | |
| 843 | lexer = lexers.Fallback | |
| 844 | } | |
| 845 | iterator, err := lexer.Tokenise(nil, source) | |
| 846 | if err != nil { | |
| 847 | return "<pre>" + template.HTMLEscapeString(source) + "</pre>" | |
| 848 | } | |
| 849 | var buf bytes.Buffer | |
| 850 | f := html.New(html.WithClasses(true)) | |
| 851 | if err := f.Format(&buf, styles.Get("friendly"), iterator); err != nil { | |
| 852 | return "<pre>" + template.HTMLEscapeString(source) + "</pre>" | |
| 853 | } | |
| 854 | return buf.String() | |
| 855 | } | |
| 833 | 856 | |
| 834 | 857 | // mdHTML renders user-authored markdown (issue and MR bodies, comments). |
| 835 | 858 | // goldmark's default renderer drops raw HTML, so this is safe as-is. |
| @@ -921,8 +944,16 @@ func renderComments(cs []store.IssueComment, md func(string) template.HTML) []re | ||
| 921 | 944 | |
| 922 | 945 | // ugcPolicy sanitizes rendered repo content before it enters the forge's |
| 923 | 946 | // origin: markdown is already safe (goldmark drops raw HTML), but org-mode |
| 924 | // output and repo-authored HTML are not. | |
| 925 | var ugcPolicy = bluemonday.UGCPolicy() | |
| 947 | // output and repo-authored HTML are not. Chroma's highlighting classes | |
| 948 | // must survive; the pattern admits only short token codes, not the site's | |
| 949 | // own class names. | |
| 950 | var ugcPolicy = func() *bluemonday.Policy { | |
| 951 | p := bluemonday.UGCPolicy() | |
| 952 | p.AllowAttrs("class"). | |
| 953 | Matching(regexp.MustCompile(`^(chroma|[a-z0-9]{1,3})( (chroma|[a-z0-9]{1,3}))*$`)). | |
| 954 | OnElements("span", "pre", "code", "div") | |
| 955 | return p | |
| 956 | }() | |
| 926 | 957 | |
| 927 | 958 | // renderReadme renders a README by extension: markdown, org-mode, and |
| 928 | 959 | // (sanitized) HTML richly; everything else as escaped plaintext. |
| @@ -942,11 +973,18 @@ func renderReadme(name string, raw []byte) template.HTML { | ||
| 942 | 973 | return template.HTML(buf.String()) |
| 943 | 974 | case ".org": |
| 944 | 975 | doc := org.New().Parse(bytes.NewReader(raw), name) |
| 945 | html, err := doc.Write(org.NewHTMLWriter()) | |
| 976 | writer := org.NewHTMLWriter() | |
| 977 | writer.HighlightCodeBlock = func(source, lang string, inline bool, params map[string]string) string { | |
| 978 | if inline { | |
| 979 | return "<code>" + template.HTMLEscapeString(source) + "</code>" | |
| 980 | } | |
| 981 | return fenceHighlight(source, lang) | |
| 982 | } | |
| 983 | out, err := doc.Write(writer) | |
| 946 | 984 | if err != nil { |
| 947 | 985 | return plain() |
| 948 | 986 | } |
| 949 | return template.HTML(ugcPolicy.Sanitize(html)) | |
| 987 | return template.HTML(ugcPolicy.Sanitize(out)) | |
| 950 | 988 | case ".html", ".htm": |
| 951 | 989 | return template.HTML(ugcPolicy.Sanitize(string(raw))) |
| 952 | 990 | default: |
internal/web/static/style.css +3 −3
| @@ -457,9 +457,9 @@ button.linklike:hover { text-decoration: underline; filter: none; } | ||
| 457 | 457 | margin-top: var(--sp-4); |
| 458 | 458 | overflow-x: auto; |
| 459 | 459 | } |
| 460 | /* chroma emits inline styles for a light background; pin code blocks to | |
| 461 | light colors in both schemes so unstyled tokens stay legible. */ | |
| 462 | .code { background: #f8f8f8; color: #1a1a1a; padding: var(--sp-3) var(--sp-4); color-scheme: light; } | |
| 460 | /* Highlighting is class-based with a palette per scheme, so code blocks | |
| 461 | follow the site's own colors. */ | |
| 462 | .code { background: var(--code-bg); color: var(--fg); padding: var(--sp-3) var(--sp-4); } | |
| 463 | 463 | .code pre { margin: 0; background: transparent !important; } |
| 464 | 464 | @media (prefers-color-scheme: dark) { |
| 465 | 465 | .code { border-color: var(--line); } |