jojo/modules/markup/markdown/transform_codeblock_lang.go
TurtleArmy 5b6c702f41 feat(ui): support Pandoc style code blocks (#12099)
This resolves https://codeberg.org/forgejo/forgejo/issues/11107.

Codeberg doesn't support [Pandoc style code blocks](https://pandoc.org/MANUAL.html#extension-fenced_code_attributes), so only the two of these 3 will have syntax highlighting.

\`\`\`haskell
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
\`\`\`

\`\`\`haskell {.numberLines}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
\`\`\`

\`\`\`{.numberLines .haskell}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
\`\`\`

```haskell
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
```

```haskell {.numberLines}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
```

```{.numberLines .haskell}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
```

This PR adds syntax highlighting to the examples with Pandoc style code blocks. It also adds redundant code to explicitly handle the second case with the trailing attribute syntax, which might be unnecessary since it already works, but I think should be fine to leave in.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12099
Reviewed-by: Ellen Εμίλια Άννα Zscheile <fogti@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
2026-05-12 00:53:09 +02:00

57 lines
1.5 KiB
Go

// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markdown
import (
"bytes"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
func (g *ASTTransformer) transformCodeblockLanguage(v *ast.FencedCodeBlock, reader text.Reader) {
if v.Info == nil {
return
}
src := reader.Source()
info := v.Info.Segment.Value(src)
// Parse Pandoc style attributes
// https://pandoc.org/MANUAL.html#extension-fenced_code_attributes
//
// For example,
// ```{.haskell .numberLines}
// ...
// ```
// Should have a language of "haskell", not "{.haskell .numberLines}"
if trimmed := bytes.TrimSpace(info); bytes.HasPrefix(trimmed, []byte{'{'}) && bytes.HasSuffix(trimmed, []byte{'}'}) {
attributes := trimmed[1 : len(trimmed)-1]
for attribute := range bytes.SplitSeq(attributes, []byte{' '}) {
if class, found := bytes.CutPrefix(attribute, []byte{'.'}); found {
if lexer := lexers.Get(string(class)); lexer != nil {
lang := class
langInx := bytes.Index(info, lang)
start := v.Info.Segment.Start + langInx
end := start + len(lang)
v.Info = ast.NewTextSegment(text.NewSegment(start, end))
return
}
}
}
return
}
// Strip language after commas
//
// For example,
// ```rust,ignore
// ...
// ```
// Should have a language of "rust", not "rust,ignore"
if i := bytes.IndexByte(info, ','); i != -1 {
start := v.Info.Segment.Start
v.Info = ast.NewTextSegment(text.NewSegment(start, start+i))
}
}