diff --git a/UPGRADING.md b/UPGRADING.md index 5f5a44ce..3ee99106 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,13 @@ Notes for upgrading an existing GoBlog site across releases that need manual steps. Newest first. +## Code highlighting moved to the server + +Fenced code in posts and pages is now coloured when the markdown is rendered, and goblog no longer loads highlight.js. Two things to do: + +- **Label your code fences.** A fence with no language (```` ``` ```` rather than ```` ```go ````) is guessed, and chroma's guess only works for code with a clear signature, such as Go's `package` line or a `#!/bin/bash` shebang. Anything else renders as plain text until you add the language: ```` ```bash ````, ```` ```python ````, ```` ```yaml ```` and so on ([chroma's list](https://github.com/alecthomas/chroma#supported-languages)). +- **Themes:** drop any `hljs.highlightAll()` call from `footer.html`; `hljs` is no longer defined, so the call throws. To restyle code, override the `.chroma` classes in your `goblog.css`; the defaults are in `www/css/base.css`. + ## Moving an existing site into `GOBLOG_DATA_DIR` Optional, from 0.14.0. A site set up before `GOBLOG_DATA_DIR` existed keeps its files in goblog's working directory (`/go/src/github.com/compscidr/goblog` in the Docker image), usually with one bind mount per path. It keeps working as it is. To move it to a single data directory instead: diff --git a/blog/render.go b/blog/render.go index 1111e499..9fe5bd8d 100644 --- a/blog/render.go +++ b/blog/render.go @@ -4,8 +4,10 @@ import ( "bytes" "html/template" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/microcosm-cc/bluemonday" "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting/v2" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" ) @@ -15,8 +17,17 @@ import ( // iframes, Instagram scripts) depend on it. This is a deliberate decision, // recorded in docs/superpowers/specs/2026-09-22-server-side-markdown-design.md; // a post or page can therefore run JavaScript on the site. +// +// Fenced code is coloured here rather than by highlight.js in the browser +// (#625). Almost no fence in existing posts names its language, so an +// unlabelled one is guessed; a guess chroma cannot make renders as plain +// text, and labelling that fence fixes it. The colours are CSS classes, +// styled in www/css/base.css so a theme can restyle them. var authorMarkdown = goldmark.New( - goldmark.WithExtensions(extension.GFM), + goldmark.WithExtensions(extension.GFM, highlighting.NewHighlighting( + highlighting.WithGuessLanguage(true), + highlighting.WithFormatOptions(chromahtml.WithClasses(true)), + )), goldmark.WithRendererOptions(html.WithUnsafe()), ) diff --git a/blog/render_test.go b/blog/render_test.go index 9a1ff05a..20896515 100644 --- a/blog/render_test.go +++ b/blog/render_test.go @@ -26,13 +26,37 @@ func TestPostHTML_KeepsRawHTML(t *testing.T) { func TestPostHTML_RendersGFM(t *testing.T) { p := Post{Content: "# Title\n\nSome **bold** and a [link](/x).\n\n| a | b |\n|---|---|\n| 1 | 2 |\n\n```go\nfunc main() {}\n```\n\n- [x] done\n- [ ] todo\n\n~~struck~~\n"} got := string(p.HTML()) - for _, want := range []string{"bold", `link`, "", "", "struck"} { + for _, want := range []string{"bold", `link`, "
1
", "", "struck"} { if !strings.Contains(got, want) { t.Errorf("missing %q in %q", want, got) } } } +// TestPostHTML_HighlightsCode: fenced code is coloured on the server (#625). +// A labelled fence uses its language, an unlabelled one is guessed, one that +// cannot be guessed is plain text, and code is escaped either way. +func TestPostHTML_HighlightsCode(t *testing.T) { + cases := map[string]string{ + "labelled": "```go\nfunc main() {}\n```\n", + "guessed": "```\npackage main\n\nfunc main() {}\n```\n", + "unguessable": "```\nhello \n```\n", + } + for name, src := range cases { + got := string(Post{Content: src}.HTML()) + if !strings.Contains(got, `
`) {
+			t.Errorf("%s: not rendered by chroma: %q", name, got)
+		}
+		if strings.Contains(got, "
   
   {{ end }}
-  {{ if or .admin_page .post }}
-  
-  
-  {{ end }}
   
   
   
diff --git a/themes/default/templates/footer.html b/themes/default/templates/footer.html
index 59106d38..1974c644 100644
--- a/themes/default/templates/footer.html
+++ b/themes/default/templates/footer.html
@@ -17,12 +17,6 @@ 

-{{ end }} {{ with index .settings "custom_footer_code" }}{{ if .Value }} {{ .Value | rawHTML }} {{ end }}{{ end }} diff --git a/www/css/base.css b/www/css/base.css index 5c4ee4ba..d5a77918 100644 --- a/www/css/base.css +++ b/www/css/base.css @@ -56,3 +56,82 @@ .striped--light-gray:nth-child(odd) { background-color: #eee; } + +/* Code highlighting. Posts and pages colour fenced code on the server with + chroma (#625); these are chroma's classes in its "github" style, generated + by chroma's html.Formatter.WriteCSS. A theme restyles code by overriding + them in its goblog.css, which loads after this file. The padding and + scrolling match what highlight.js gave code blocks before. */ +.chroma { overflow-x: auto; padding: 1em; } +/* PreWrapper */ .chroma { background-color: #f7f7f7; -webkit-text-size-adjust: none; } +/* Error */ .chroma .err { color: #f6f8fa; background-color: #82071e } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #dedede } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #cf222e } +/* KeywordConstant */ .chroma .kc { color: #cf222e } +/* KeywordDeclaration */ .chroma .kd { color: #cf222e } +/* KeywordNamespace */ .chroma .kn { color: #cf222e } +/* KeywordPseudo */ .chroma .kp { color: #cf222e } +/* KeywordReserved */ .chroma .kr { color: #cf222e } +/* KeywordType */ .chroma .kt { color: #cf222e } +/* NameAttribute */ .chroma .na { color: #1f2328 } +/* NameClass */ .chroma .nc { color: #1f2328 } +/* NameConstant */ .chroma .no { color: #0550ae } +/* NameDecorator */ .chroma .nd { color: #0550ae } +/* NameEntity */ .chroma .ni { color: #6639ba } +/* NameLabel */ .chroma .nl { color: #990000; font-weight: bold } +/* NameNamespace */ .chroma .nn { color: #24292e } +/* NameOther */ .chroma .nx { color: #1f2328 } +/* NameTag */ .chroma .nt { color: #0550ae } +/* NameBuiltin */ .chroma .nb { color: #6639ba } +/* NameBuiltinPseudo */ .chroma .bp { color: #6a737d } +/* NameVariable */ .chroma .nv { color: #953800 } +/* NameVariableClass */ .chroma .vc { color: #953800 } +/* NameVariableGlobal */ .chroma .vg { color: #953800 } +/* NameVariableInstance */ .chroma .vi { color: #953800 } +/* NameVariableMagic */ .chroma .vm { color: #953800 } +/* NameFunction */ .chroma .nf { color: #6639ba } +/* NameFunctionMagic */ .chroma .fm { color: #6639ba } +/* LiteralString */ .chroma .s { color: #0a3069 } +/* LiteralStringAffix */ .chroma .sa { color: #0a3069 } +/* LiteralStringBacktick */ .chroma .sb { color: #0a3069 } +/* LiteralStringChar */ .chroma .sc { color: #0a3069 } +/* LiteralStringDelimiter */ .chroma .dl { color: #0a3069 } +/* LiteralStringDoc */ .chroma .sd { color: #0a3069 } +/* LiteralStringDouble */ .chroma .s2 { color: #0a3069 } +/* LiteralStringEscape */ .chroma .se { color: #0a3069 } +/* LiteralStringHeredoc */ .chroma .sh { color: #0a3069 } +/* LiteralStringInterpol */ .chroma .si { color: #0a3069 } +/* LiteralStringOther */ .chroma .sx { color: #0a3069 } +/* LiteralStringRegex */ .chroma .sr { color: #0a3069 } +/* LiteralStringSingle */ .chroma .s1 { color: #0a3069 } +/* LiteralStringSymbol */ .chroma .ss { color: #032f62 } +/* LiteralNumber */ .chroma .m { color: #0550ae } +/* LiteralNumberBin */ .chroma .mb { color: #0550ae } +/* LiteralNumberFloat */ .chroma .mf { color: #0550ae } +/* LiteralNumberHex */ .chroma .mh { color: #0550ae } +/* LiteralNumberInteger */ .chroma .mi { color: #0550ae } +/* LiteralNumberIntegerLong */ .chroma .il { color: #0550ae } +/* LiteralNumberOct */ .chroma .mo { color: #0550ae } +/* Operator */ .chroma .o { color: #0550ae } +/* OperatorWord */ .chroma .ow { color: #0550ae } +/* OperatorReserved */ .chroma .or { color: #0550ae } +/* Punctuation */ .chroma .p { color: #1f2328 } +/* Comment */ .chroma .c { color: #57606a } +/* CommentHashbang */ .chroma .ch { color: #57606a } +/* CommentMultiline */ .chroma .cm { color: #57606a } +/* CommentSingle */ .chroma .c1 { color: #57606a } +/* CommentSpecial */ .chroma .cs { color: #57606a } +/* CommentPreproc */ .chroma .cp { color: #57606a } +/* CommentPreprocFile */ .chroma .cpf { color: #57606a } +/* GenericDeleted */ .chroma .gd { color: #82071e; background-color: #ffebe9 } +/* GenericEmph */ .chroma .ge { color: #1f2328 } +/* GenericInserted */ .chroma .gi { color: #116329; background-color: #dafbe1 } +/* GenericOutput */ .chroma .go { color: #1f2328 } +/* GenericUnderline */ .chroma .gl { text-decoration: underline } +/* TextWhitespace */ .chroma .w { color: #ffffff }

1