Repository: gohugoio/hugo (language: go)

Issue:
markup/highlight: Adjust inline markup
See #9634
Closes #9635



Discussion / hints:
I think this change will work. I would only suggest that the display:flex; regex be made stricter.

If a user does `{{< highlight text "hl_inline=true" >}}display:flex;{{< /highlight >}}`, it will wipe off the content "display:flex;" as well!

Suggestion: 

```
replaceRE `<span style="display:flex;">` `<span>` ..
```

or

```
replaceRE `<span style="(.*?)display:flex;(.*?)">` `<span style="${1}${2}">` ..
```

With that, even if the user has `<span style="display:flex;">` in their inline block content, that will stay safe as that will be HTML escaped first to `&lt;span style=&#34;display:flex;&#34;&gt;`.
@kaushalmodi did you say something somewhere about the flex style?
> did you say something somewhere about the flex style?

I haven't yet, but given that the current regex `display:flex;` doesn't have any special HTML chars like `<`, it can easily match that in the `highlight` shortcode content text too.

@bep I had opened an issue on chroma regarding `display:flex;`, and I got a reply: https://github.com/alecthomas/chroma/issues/617#issuecomment-1063251066

Does this help from there?

> You should also be able to override the line style [like this](https://github.com/alecthomas/chroma/blob/7bfe2f41c6bbf6268ab06709b1361dc09f66b08a/cmd/chroma/main.go#L202), using chroma.Line class.
See my comment in https://github.com/alecthomas/chroma/issues/617#issuecomment-1063822264

We need to wait for a fix upstream, so I need to revert this "inline thing", but I'll keep this PR open so I can finish it when time is right.
@bep This fix is waiting in the Chroma PR, and will need Hugo to use the new `InlineCode` option: https://github.com/alecthomas/chroma/pull/618 for Inline highlighting. Can you review that PR?

Thanks. 
@bep This PR was feature complete. Only the regex part was hacky, but those are internal details. Can you please merge this feature in, and then update the internal Chroma API when that gets fixed upstream?
@bep Can you please take another look at this? The feature was working great with with regex fix 😃 . I'll really appreciate if this feature can go in first. When a proper fix arrives from upstream Chroma, that fix will be transparent to the users.

Fix patch:
diff --git a/go.mod b/go.mod
index 96018fd7043..33ec55a3464 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/gohugoio/hugo
 require (
 	github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
 	github.com/PuerkitoBio/purell v1.1.1
-	github.com/alecthomas/chroma/v2 v2.1.0
+	github.com/alecthomas/chroma/v2 v2.2.0
 	github.com/armon/go-radix v1.0.0
 	github.com/aws/aws-sdk-go v1.43.5
 	github.com/bep/clock v0.3.0
diff --git a/go.sum b/go.sum
index ea2caf8349d..a6692162761 100644
--- a/go.sum
+++ b/go.sum
@@ -133,6 +133,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/alecthomas/chroma/v2 v2.1.0 h1:ZG9L5/RsxO/xIONrBy8Cgo+5si3d9x3osweXc4VHl0o=
 github.com/alecthomas/chroma/v2 v2.1.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
+github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
+github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
index 80991f21bce..d55958d355b 100644
--- a/markup/highlight/config.go
+++ b/markup/highlight/config.go
@@ -72,6 +72,9 @@ type Config struct {
 	// A space separated list of line numbers, e.g. “3-8 10-20”.
 	Hl_Lines string
 
+	// If set, the markup will not be wrapped in any container.
+	Hl_inline bool
+
 	// A parsed and ready to use list of line ranges.
 	HL_lines_parsed [][2]int `json:"-"`
 
@@ -93,6 +96,7 @@ func (cfg Config) ToHTMLOptions() []html.Option {
 		html.LineNumbersInTable(cfg.LineNumbersInTable),
 		html.WithClasses(!cfg.NoClasses),
 		html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
+		html.InlineCode(cfg.Hl_inline),
 	}
 
 	if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index 7e570413280..5b19d6e8e66 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -88,6 +88,7 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a
 	var b strings.Builder
 
 	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
+
 	options := ctx.Options()
 
 	if err := applyOptionsFromMap(options, &cfg); err != nil {
@@ -108,8 +109,13 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a
 		return HightlightResult{}, err
 	}
 
+	highlighted := b.String()
+	if high == 0 {
+		high = len(highlighted)
+	}
+
 	return HightlightResult{
-		highlighted: template.HTML(b.String()),
+		highlighted: template.HTML(highlighted),
 		innerLow:    low,
 		innerHigh:   high,
 	}, nil
@@ -117,6 +123,7 @@ func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts a
 
 func (h chromaHighlighter) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
 	cfg := h.cfg
+
 	attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
 
 	if err := applyOptionsFromMap(ctx.Options(), &cfg); err != nil {
@@ -158,8 +165,6 @@ func (h HightlightResult) Inner() template.HTML {
 }
 
 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)
@@ -176,11 +181,15 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.
 	w := &byteCountFlexiWriter{delegate: fw}
 
 	if lexer == nil {
-		wrapper := getPreWrapper(lang, w)
-		fmt.Fprint(w, wrapper.Start(true, ""))
-		fmt.Fprint(w, gohtml.EscapeString(code))
-		fmt.Fprint(w, wrapper.End(true))
-		return low, high, nil
+		if cfg.Hl_inline {
+			fmt.Fprint(w, fmt.Sprintf("<code%s>%s</code>", inlineCodeAttrs(lang), gohtml.EscapeString(code)))
+		} else {
+			preWrapper := getPreWrapper(lang, w)
+			fmt.Fprint(w, preWrapper.Start(true, ""))
+			fmt.Fprint(w, gohtml.EscapeString(code))
+			fmt.Fprint(w, preWrapper.End(true))
+		}
+		return 0, 0, nil
 	}
 
 	style := styles.Get(cfg.Style)
@@ -194,20 +203,51 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes.
 		return 0, 0, err
 	}
 
+	if !cfg.Hl_inline {
+		writeDivStart(w, attributes)
+	}
+
 	options := cfg.ToHTMLOptions()
-	preWrapper := getPreWrapper(lang, w)
-	options = append(options, html.WithPreWrapper(preWrapper))
+	var wrapper html.PreWrapper
+
+	if cfg.Hl_inline {
+		wrapper = startEnd{
+			start: func(code bool, styleAttr string) string {
+				if code {
+					return fmt.Sprintf(`<code%s>`, inlineCodeAttrs(lang))
+				}
+				return ``
+			},
+			end: func(code bool) string {
+				if code {
+					return `</code>`
+				}
+
+				return ``
+			},
+		}
 
-	formatter := html.New(options...)
+	} else {
+		wrapper = getPreWrapper(lang, w)
+	}
+
+	options = append(options, html.WithPreWrapper(wrapper))
 
-	writeDivStart(w, attributes)
+	formatter := html.New(options...)
 
 	if err := formatter.Format(w, style, iterator); err != nil {
 		return 0, 0, err
 	}
-	writeDivEnd(w)
 
-	return preWrapper.low, preWrapper.high, nil
+	if !cfg.Hl_inline {
+		writeDivEnd(w)
+	}
+
+	if p, ok := wrapper.(*preWrapper); ok {
+		return p.low, p.high, nil
+	}
+
+	return 0, 0, nil
 }
 
 func getPreWrapper(language string, writeCounter *byteCountFlexiWriter) *preWrapper {
@@ -232,6 +272,12 @@ func (p *preWrapper) Start(code bool, styleAttr string) string {
 	return w.String()
 }
 
+func inlineCodeAttrs(lang string) string {
+	if lang == "" {
+	}
+	return fmt.Sprintf(` class="code-inline language-%s"`, lang)
+}
+
 func WritePreStart(w io.Writer, language, styleAttr string) {
 	fmt.Fprintf(w, `<pre tabindex="0"%s>`, styleAttr)
 	fmt.Fprint(w, "<code")
@@ -249,6 +295,19 @@ func (p *preWrapper) End(code bool) string {
 	return preEnd
 }
 
+type startEnd struct {
+	start func(code bool, styleAttr string) string
+	end   func(code bool) string
+}
+
+func (s startEnd) Start(code bool, styleAttr string) string {
+	return s.start(code, styleAttr)
+}
+
+func (s startEnd) End(code bool) string {
+	return s.end(code)
+}
+
 func WritePreEnd(w io.Writer) {
 	fmt.Fprint(w, preEnd)
 }
diff --git a/markup/internal/attributes/attributes.go b/markup/internal/attributes/attributes.go
index edf85782204..6887409834e 100644
--- a/markup/internal/attributes/attributes.go
+++ b/markup/internal/attributes/attributes.go
@@ -30,6 +30,7 @@ var chromaHightlightProcessingAttributes = map[string]bool{
 	"anchorLineNos":      true,
 	"guessSyntax":        true,
 	"hl_Lines":           true,
+	"hl_inline":          true,
 	"lineAnchors":        true,
 	"lineNos":            true,
 	"lineNoStart":        true,


Test patch:
diff --git a/markup/highlight/integration_test.go b/markup/highlight/integration_test.go
new file mode 100644
index 00000000000..2b4379bc2ca
--- /dev/null
+++ b/markup/highlight/integration_test.go
@@ -0,0 +1,85 @@
+// Copyright 2022 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package highlight_test
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/hugolib"
+)
+
+func TestHighlightInline(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+[markup]
+[markup.highlight]
+codeFences = true
+noClasses = false
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Inline in Shortcode
+
+Inline:{{< highlight emacs "hl_inline=true" >}}(message "this highlight shortcode"){{< /highlight >}}:End.
+Inline Unknown:{{< highlight foo "hl_inline=true" >}}(message "this highlight shortcode"){{< /highlight >}}:End.
+
+## Inline in code block
+
+Not sure if this makes sense, but add a test for it:
+
+§§§bash {hl_inline=true}
+(message "highlight me")
+§§§
+
+## HighlightCodeBlock in hook
+
+§§§html
+(message "highlight me 2")
+§§§
+
+## Unknown lexer
+
+§§§foo {hl_inline=true}
+(message "highlight me 3")
+§§§
+
+
+-- layouts/_default/_markup/render-codeblock-html.html --
+{{ $opts := dict "hl_inline" true }}
+{{ $result := transform.HighlightCodeBlock . $opts }}
+HighlightCodeBlock: Wrapped:{{ $result.Wrapped  }}|Inner:{{ $result.Inner }}
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   false,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html",
+		"Inline:<code class=\"code-inline language-emacs\"><span class=\"p\">(</span><span class=\"nf\">message</span> <span class=\"s\">&#34;this highlight shortcode&#34;</span><span class=\"p\">)</span></code>:End.",
+		"Inline Unknown:<code class=\"code-inline language-foo\">(message &#34;this highlight shortcode&#34;)</code>:End.",
+		"Not sure if this makes sense, but add a test for it:</p>\n<code class=\"code-inline language-bash\"><span class=\"o\">(</span>message <span class=\"s2\">&#34;highlight me&#34;</span><span class=\"o\">)</span>\n</code>",
+		"HighlightCodeBlock: Wrapped:<code class=\"code-inline language-html\">(message &#34;highlight me 2&#34;)</code>|Inner:<code class=\"code-inline language-html\">(message &#34;highlight me 2&#34;)</code>",
+		"<code class=\"code-inline language-foo\">(message &#34;highlight me 3&#34;)\n</code>",
+	)
+}