Repository: gohugoio/hugo (language: go)

Issue:
Expose .ChromaLexer (or boolean) within the codeblock render hook template
With a generic hook (render-codeblock.html), it would helpful to know if `.Lang` is a known Chroma lexer. We could return the lexer used (or nil) , or a boolean. 

Something like...

```
{{ if .KnownChromaLexer }}
  {{ $r := transform.HighlightCodeBlock . }}
  {{ $r.Body }}
{{ else }}
  my stuff
{{ end }}
```




Discussion / hints:
How about `IsChroma`?

OK, looking back at `transform.HighlightCodeBlock` is also suspect we also need to adjust that a little. I notice that the returned struct has a `Body` and a `Highlighted` method that currently just returns `Body`. With this it's not possible, I think, for a user to render its own `<div>`, so I suggest we:

1. Let `Body` be ... the inner parts.
2. Let `Highlighted` return the full `<div>` with HTML attributes.
Not that we will ever replace Chroma as the highlighter of choice, but how about `CanHighlight`, meaning "Chroma has a lexer for this language".
The name game... I'll give it a shot.

The input is some code. Or some diagram syntax. Or some phone numbers. It could be anything. While the markdown element is known as a [fenced codeblock](https://spec.commonmark.org/0.28/#fenced-code-blocks), what's inside the fences is not necessarily code. Would `.Inner` be a better choice (generic rather than precise, parallel to shortcode syntax)?

If we can perform syntax highlighting on `.Inner`, then by definition it is code.

`.Inner` --> Chroma --> `.Code`
`.Inner` --> Chroma --> `.Code` --> Wrap With Div --> `.WrappedCode`

OR 

`.Inner` --> Chroma --> `.HighlightedCode`
`.Inner` --> Chroma --> `.HighlightedCode` --> Wrap With Div --> `.WrappedHighlightedCode`

I've been rethinking `.CanHighlight` or whatever...

I'm not sure it makes sense to have this method available in the root context of the template. Within the render hook we "can highlight" anything we want, as long as we transform/exchange `.Lang` into something that Chroma can handle. 

What I really want to know: is this language a Chroma lexer or an alias to a chroma lexer? 

```text
.Lang.HasLexer
```

This is technically accurate, and not tied to Chroma.
I want to clarify this statement:

> Within the render hook we "can highlight" anything we want

This example has come up at least once, either here or on the forum. The user wants to highlight log files, and found that the best available lexer is for `ini` files.

````
```log 
2022-02-26T09:37:26-08:00 Backup started
2022-02-26T09:38:31-08:00 Backup failed: unable to connect to 93.184.216.34
```
````

Chroma does not have a `log` lexer, for obvious reasons, but the string inside the code fences _can_ be highlighted. As I write the code to handle this, I run into a problem:

```
{{ $lang := .Lang }}
{{ if eq $lang "log" }}
  {{ $lang = "ini" }}
{{ end }}
{{ if $lang | hasLexer }} <-- I think this would need to be a function
  ...
{{ end }}
```






I hate naming things.

I will postpone this until Monday, but the last comment before I put away the computer:

* We currently have the highlight tucked away inside `transform.Highlight`, which I suspect was convenient ...
* But now I think it deserves its own namespace.
* So we can do `{{ if $lang | highlight.Supports }}` or something
* Also`{{ highlight.Apply }}` or something

Sleep on that ...

Fix patch:
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index c04fbad314f..892cb72eed4 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -75,7 +75,7 @@ func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (strin
 	}
 	var b strings.Builder
 
-	if err := highlight(&b, code, lang, nil, cfg); err != nil {
+	if _, _, err := highlight(&b, code, lang, nil, cfg); err != nil {
 		return "", err
 	}
 
@@ -103,13 +103,15 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts i
 		return HightlightResult{}, err
 	}
 
-	err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg)
+	low, high, err := highlight(&b, ctx.Inner(), ctx.Type(), attributes, cfg)
 	if err != nil {
 		return HightlightResult{}, err
 	}
 
 	return HightlightResult{
-		Body: template.HTML(b.String()),
+		highlighted: template.HTML(b.String()),
+		innerLow:    low,
+		innerHigh:   high,
 	}, nil
 }
 
@@ -127,7 +129,8 @@ func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.Codebl
 
 	code := text.Puts(ctx.Inner())
 
-	return highlight(w, code, ctx.Type(), attributes, cfg)
+	_, _, err := highlight(w, code, ctx.Type(), attributes, cfg)
+	return err
 }
 
 func (h chromaHighlighter) IsDefaultCodeBlockRenderer() bool {
@@ -141,14 +144,22 @@ func (h chromaHighlighter) GetIdentity() identity.Identity {
 }
 
 type HightlightResult struct {
-	Body template.HTML
+	innerLow    int
+	innerHigh   int
+	highlighted template.HTML
 }
 
-func (h HightlightResult) Highlighted() template.HTML {
-	return h.Body
+func (h HightlightResult) Wrapped() template.HTML {
+	return h.highlighted
 }
 
-func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error {
+func (h HightlightResult) Inner() template.HTML {
+	return h.highlighted[h.innerLow:h.innerHigh]
+}
+
+func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) (int, int, error) {
+	var low, high int
+
 	var lexer chroma.Lexer
 	if lang != "" {
 		lexer = lexers.Get(lang)
@@ -162,12 +173,14 @@ func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.A
 		lang = strings.ToLower(lexer.Config().Name)
 	}
 
+	w := &byteCountFlexiWriter{delegate: fw}
+
 	if lexer == nil {
-		wrapper := getPreWrapper(lang)
+		wrapper := getPreWrapper(lang, w)
 		fmt.Fprint(w, wrapper.Start(true, ""))
 		fmt.Fprint(w, gohtml.EscapeString(code))
 		fmt.Fprint(w, wrapper.End(true))
-		return nil
+		return low, high, nil
 	}
 
 	style := styles.Get(cfg.Style)
@@ -178,42 +191,44 @@ func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.A
 
 	iterator, err := lexer.Tokenise(nil, code)
 	if err != nil {
-		return err
+		return 0, 0, err
 	}
 
 	options := cfg.ToHTMLOptions()
-	options = append(options, getHtmlPreWrapper(lang))
+	preWrapper := getPreWrapper(lang, w)
+	options = append(options, html.WithPreWrapper(preWrapper))
 
 	formatter := html.New(options...)
 
 	writeDivStart(w, attributes)
+
 	if err := formatter.Format(w, style, iterator); err != nil {
-		return err
+		return 0, 0, err
 	}
 	writeDivEnd(w)
 
-	return nil
+	return preWrapper.low, preWrapper.high, nil
 }
 
-func getPreWrapper(language string) preWrapper {
-	return preWrapper{language: language}
-}
-
-func getHtmlPreWrapper(language string) html.Option {
-	return html.WithPreWrapper(getPreWrapper(language))
+func getPreWrapper(language string, writeCounter *byteCountFlexiWriter) *preWrapper {
+	return &preWrapper{language: language, writeCounter: writeCounter}
 }
 
 type preWrapper struct {
-	language string
+	low          int
+	high         int
+	writeCounter *byteCountFlexiWriter
+	language     string
 }
 
-func (p preWrapper) Start(code bool, styleAttr string) string {
+func (p *preWrapper) Start(code bool, styleAttr string) string {
 	var language string
 	if code {
 		language = p.language
 	}
 	w := &strings.Builder{}
 	WritePreStart(w, language, styleAttr)
+	p.low = p.writeCounter.counter + w.Len()
 	return w.String()
 }
 
@@ -229,7 +244,8 @@ func WritePreStart(w io.Writer, language, styleAttr string) {
 
 const preEnd = "</code></pre>"
 
-func (p preWrapper) End(code bool) string {
+func (p *preWrapper) End(code bool) string {
+	p.high = p.writeCounter.counter
 	return preEnd
 }
 
@@ -258,3 +274,31 @@ func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
 func writeDivEnd(w hugio.FlexiWriter) {
 	w.WriteString("</div>")
 }
+
+type byteCountFlexiWriter struct {
+	delegate hugio.FlexiWriter
+	counter  int
+}
+
+func (w *byteCountFlexiWriter) Write(p []byte) (int, error) {
+	n, err := w.delegate.Write(p)
+	w.counter += n
+	return n, err
+}
+
+func (w *byteCountFlexiWriter) WriteByte(c byte) error {
+	w.counter++
+	return w.delegate.WriteByte(c)
+}
+
+func (w *byteCountFlexiWriter) WriteString(s string) (int, error) {
+	n, err := w.delegate.WriteString(s)
+	w.counter += n
+	return n, err
+}
+
+func (w *byteCountFlexiWriter) WriteRune(r rune) (int, error) {
+	n, err := w.delegate.WriteRune(r)
+	w.counter += n
+	return n, err
+}
diff --git a/tpl/diagrams/diagrams.go b/tpl/diagrams/diagrams.go
index b0ddf5a42f1..60ebfb902fd 100644
--- a/tpl/diagrams/diagrams.go
+++ b/tpl/diagrams/diagrams.go
@@ -25,13 +25,13 @@ import (
 )
 
 type SVGDiagram interface {
+	// Wrapped returns the diagram as an SVG, including the <svg> container.
+	Wrapped() template.HTML
+
 	// Inner returns the inner markup of the SVG.
 	// This allows for the <svg> container to be created manually.
 	Inner() template.HTML
 
-	// SVG returns the diagram as an SVG, including the <svg> container.
-	SVG() template.HTML
-
 	// Width returns the width of the SVG.
 	Width() int
 
@@ -47,7 +47,7 @@ func (d goatDiagram) Inner() template.HTML {
 	return template.HTML(d.d.Body)
 }
 
-func (d goatDiagram) SVG() template.HTML {
+func (d goatDiagram) Wrapped() template.HTML {
 	return template.HTML(d.d.String())
 }
 
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index dc7cc0342c0..48cfaffffe0 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -18,6 +18,7 @@ import (
 	"html"
 	"html/template"
 
+	"github.com/alecthomas/chroma/lexers"
 	"github.com/gohugoio/hugo/cache/namedmemcache"
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/markup/converter/hooks"
@@ -90,6 +91,11 @@ func (ns *Namespace) HighlightCodeBlock(ctx hooks.CodeblockContext, opts ...inte
 	return hl.HighlightCodeBlock(ctx, optsv)
 }
 
+// CanHighlight returns whether the given language is supported by the Chroma highlighter.
+func (ns *Namespace) CanHighlight(lang string) bool {
+	return lexers.Get(lang) != nil
+}
+
 // HTMLEscape returns a copy of s with reserved HTML characters escaped.
 func (ns *Namespace) HTMLEscape(s interface{}) (string, error) {
 	ss, err := cast.ToStringE(s)


Test patch:
diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go
index fcd406c2cb3..1a70b0bf0ab 100644
--- a/markup/goldmark/codeblocks/integration_test.go
+++ b/markup/goldmark/codeblocks/integration_test.go
@@ -41,7 +41,7 @@ func TestCodeblocks(t *testing.T) {
     tabWidth = 4
 -- layouts/_default/_markup/render-codeblock-goat.html --
 {{ $diagram := diagrams.Goat .Inner }}
-Goat SVG:{{ substr $diagram.SVG 0 100 | safeHTML }}  }}|
+Goat SVG:{{ substr $diagram.Wrapped 0 100 | safeHTML }}  }}|
 Goat Attribute: {{ .Attributes.width}}|
 -- layouts/_default/_markup/render-codeblock-go.html --
 Go Code: {{ .Inner | safeHTML }}|
@@ -113,6 +113,57 @@ Go Language: golang|
 	)
 }
 
+func TestHighlightCodeblock(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+[markup]
+[markup.highlight]
+anchorLineNos = false
+codeFences = true
+guessSyntax = false
+hl_Lines = ''
+lineAnchors = ''
+lineNoStart = 1
+lineNos = false
+lineNumbersInTable = true
+noClasses = false
+style = 'monokai'
+tabWidth = 4
+-- layouts/_default/_markup/render-codeblock.html --
+{{ $result := transform.HighlightCodeBlock . }}
+Inner: |{{ $result.Inner | safeHTML }}|
+Wrapped: |{{ $result.Wrapped | safeHTML }}|
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Go Code
+
+§§§go
+fmt.Println("Hello, World!");
+§§§
+
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   false,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html",
+		"Inner: |<span class=\"line\"><span class=\"cl\"><span class=\"nx\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Println</span><span class=\"p\">(</span><span class=\"s\">&#34;Hello, World!&#34;</span><span class=\"p\">);</span></span></span>|",
+		"Wrapped: |<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-go\" data-lang=\"go\"><span class=\"line\"><span class=\"cl\"><span class=\"nx\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Println</span><span class=\"p\">(</span><span class=\"s\">&#34;Hello, World!&#34;</span><span class=\"p\">);</span></span></span></code></pre></div>|",
+	)
+}
+
 func TestCodeChomp(t *testing.T) {
 	t.Parallel()
 
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index 3ccf1a2700a..289674bf176 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -95,6 +95,16 @@ func TestHighlight(t *testing.T) {
 	}
 }
 
+func TestCanHighlight(t *testing.T) {
+	t.Parallel()
+
+	c := qt.New(t)
+	ns := &transform.Namespace{}
+
+	c.Assert(ns.CanHighlight("go"), qt.Equals, true)
+	c.Assert(ns.CanHighlight("foo"), qt.Equals, false)
+}
+
 func TestHTMLEscape(t *testing.T) {
 	t.Parallel()
 	b := hugolib.NewIntegrationTestBuilder(