]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Improve shortcode indentation handling
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 28 May 2022 11:18:50 +0000 (13:18 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 30 May 2022 09:32:55 +0000 (11:32 +0200)
* Record the leading whitespace (tabs, spaces) before the shortcode when parsing the page.
* Apply that indentation to the rendered result of shortcodes without inner content (where the user will apply indentation).

Fixes #9946

common/text/transform.go
common/text/transform_test.go
hugolib/shortcode.go
hugolib/shortcode_test.go
parser/pageparser/item.go
parser/pageparser/itemtype_string.go
parser/pageparser/pagelexer.go
parser/pageparser/pageparser.go
parser/pageparser/pageparser_shortcode_test.go

index b324b54c12f4ca82efba536236c83098309f4beb..2b05b9b4f4e6043774d9721126c5ee01a1e69c2d 100644 (file)
@@ -61,3 +61,17 @@ func Puts(s string) string {
        }
        return s + "\n"
 }
+
+// VisitLinesAfter calls the given function for each line, including newlines, in the given string.
+func VisitLinesAfter(s string, fn func(line string)) {
+       high := strings.Index(s, "\n")
+       for high != -1 {
+               fn(s[:high+1])
+               s = s[high+1:]
+               high = strings.Index(s, "\n")
+       }
+
+       if s != "" {
+               fn(s)
+       }
+}
index 992dd524c80542fe796bd4e7c47d4566c52e9285..10738aee701f870153aaba40edc922fae7bb5453 100644 (file)
@@ -41,3 +41,21 @@ func TestPuts(t *testing.T) {
        c.Assert(Puts("\nA\n"), qt.Equals, "\nA\n")
        c.Assert(Puts(""), qt.Equals, "")
 }
+
+func TestVisitLinesAfter(t *testing.T) {
+       const lines = `line 1
+line 2
+
+line 3`
+
+       var collected []string
+
+       VisitLinesAfter(lines, func(s string) {
+               collected = append(collected, s)
+       })
+
+       c := qt.New(t)
+
+       c.Assert(collected, qt.DeepEquals, []string{"line 1\n", "line 2\n", "\n", "line 3"})
+
+}
index e718d861ac811621d639347e858faa0c33a3274d..b822ecfe34542ae8f18a73362570e4c654227a05 100644 (file)
@@ -170,6 +170,8 @@ type shortcode struct {
        ordinal   int
        err       error
 
+       indentation string // indentation from source.
+
        info   tpl.Info       // One of the output formats (arbitrary)
        templs []tpl.Template // All output formats
 
@@ -398,6 +400,22 @@ func renderShortcode(
                return "", false, fe
        }
 
+       if len(sc.inner) == 0 && len(sc.indentation) > 0 {
+               b := bp.GetBuffer()
+               i := 0
+               text.VisitLinesAfter(result, func(line string) {
+                       // The first line is correctly indented.
+                       if i > 0 {
+                               b.WriteString(sc.indentation)
+                       }
+                       i++
+                       b.WriteString(line)
+               })
+
+               result = b.String()
+               bp.PutBuffer(b)
+       }
+
        return result, hasVariants, err
 }
 
@@ -447,6 +465,15 @@ func (s *shortcodeHandler) extractShortcode(ordinal, level int, pt *pageparser.I
        }
        sc := &shortcode{ordinal: ordinal}
 
+       // Back up one to identify any indentation.
+       if pt.Pos() > 0 {
+               pt.Backup()
+               item := pt.Next()
+               if item.IsIndentation() {
+                       sc.indentation = string(item.Val)
+               }
+       }
+
        cnt := 0
        nestedOrdinal := 0
        nextLevel := level + 1
index 372e1be9b050d1b1368084aaa5b352db88c8f14d..cafe76703dc43cabe691d1064df2bc1d697580ab 100644 (file)
@@ -942,3 +942,76 @@ title: "p1"
        `)
 
 }
+
+func TestShortcodePreserveIndentation(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- config.toml --
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## List With Indented Shortcodes
+
+1. List 1
+    {{% mark1 %}}
+       1. Item Mark1 1
+       1. Item Mark1 2
+       {{% mark2 %}}
+       {{% /mark1 %}}
+-- layouts/shortcodes/mark1.md --
+{{ .Inner }}
+-- layouts/shortcodes/mark2.md --
+1. Item Mark2 1
+1. Item Mark2 2
+   1. Item Mark2 2-1
+1. Item Mark2 3
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+       b := NewIntegrationTestBuilder(
+               IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+                       Running:     true,
+               },
+       ).Build()
+
+       b.AssertFileContent("public/p1/index.html", "<ol>\n<li>\n<p>List 1</p>\n<ol>\n<li>Item Mark1 1</li>\n<li>Item Mark1 2</li>\n<li>Item Mark2 1</li>\n<li>Item Mark2 2\n<ol>\n<li>Item Mark2 2-1</li>\n</ol>\n</li>\n<li>Item Mark2 3</li>\n</ol>\n</li>\n</ol>")
+
+}
+
+func TestShortcodeCodeblockIndent(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- config.toml --
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Code block
+
+    {{% code %}}
+
+-- layouts/shortcodes/code.md --
+echo "foo";
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+       b := NewIntegrationTestBuilder(
+               IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+                       Running:     true,
+               },
+       ).Build()
+
+       b.AssertFileContent("public/p1/index.html", "<pre><code>echo &quot;foo&quot;;\n</code></pre>")
+
+}
index f23341ea8a768fdcf53b28b7655e5a2b05b5e809..52546be41939a35f81df858f8ea024a1c2a486df 100644 (file)
@@ -18,6 +18,8 @@ import (
        "fmt"
        "regexp"
        "strconv"
+
+       "github.com/yuin/goldmark/util"
 )
 
 type Item struct {
@@ -64,7 +66,11 @@ func (i Item) ValTyped() any {
 }
 
 func (i Item) IsText() bool {
-       return i.Type == tText
+       return i.Type == tText || i.Type == tIndentation
+}
+
+func (i Item) IsIndentation() bool {
+       return i.Type == tIndentation
 }
 
 func (i Item) IsNonWhitespace() bool {
@@ -125,6 +131,8 @@ func (i Item) String() string {
                return "EOF"
        case i.Type == tError:
                return string(i.Val)
+       case i.Type == tIndentation:
+               return fmt.Sprintf("%s:[%s]", i.Type, util.VisualizeSpaces(i.Val))
        case i.Type > tKeywordMarker:
                return fmt.Sprintf("<%s>", i.Val)
        case len(i.Val) > 50:
@@ -159,6 +167,8 @@ const (
        tScParam
        tScParamVal
 
+       tIndentation
+
        tText // plain text
 
        // preserved for later - keywords come after this
index 632afaecc00b5392273313ecf6819c8cf9eb479c..b0b849adecf29b5c5dc664f1daf063d3e9e5a092 100644 (file)
@@ -4,9 +4,36 @@ package pageparser
 
 import "strconv"
 
-const _ItemType_name = "tErrortEOFTypeHTMLStartTypeLeadSummaryDividerTypeFrontMatterYAMLTypeFrontMatterTOMLTypeFrontMatterJSONTypeFrontMatterORGTypeEmojiTypeIgnoretLeftDelimScNoMarkuptRightDelimScNoMarkuptLeftDelimScWithMarkuptRightDelimScWithMarkuptScClosetScNametScNameInlinetScParamtScParamValtTexttKeywordMarker"
+func _() {
+       // An "invalid array index" compiler error signifies that the constant values have changed.
+       // Re-run the stringer command to generate them again.
+       var x [1]struct{}
+       _ = x[tError-0]
+       _ = x[tEOF-1]
+       _ = x[TypeLeadSummaryDivider-2]
+       _ = x[TypeFrontMatterYAML-3]
+       _ = x[TypeFrontMatterTOML-4]
+       _ = x[TypeFrontMatterJSON-5]
+       _ = x[TypeFrontMatterORG-6]
+       _ = x[TypeEmoji-7]
+       _ = x[TypeIgnore-8]
+       _ = x[tLeftDelimScNoMarkup-9]
+       _ = x[tRightDelimScNoMarkup-10]
+       _ = x[tLeftDelimScWithMarkup-11]
+       _ = x[tRightDelimScWithMarkup-12]
+       _ = x[tScClose-13]
+       _ = x[tScName-14]
+       _ = x[tScNameInline-15]
+       _ = x[tScParam-16]
+       _ = x[tScParamVal-17]
+       _ = x[tIndentation-18]
+       _ = x[tText-19]
+       _ = x[tKeywordMarker-20]
+}
+
+const _ItemType_name = "tErrortEOFTypeLeadSummaryDividerTypeFrontMatterYAMLTypeFrontMatterTOMLTypeFrontMatterJSONTypeFrontMatterORGTypeEmojiTypeIgnoretLeftDelimScNoMarkuptRightDelimScNoMarkuptLeftDelimScWithMarkuptRightDelimScWithMarkuptScClosetScNametScNameInlinetScParamtScParamValtIndentationtTexttKeywordMarker"
 
-var _ItemType_index = [...]uint16{0, 6, 10, 23, 45, 64, 83, 102, 120, 129, 139, 159, 180, 202, 225, 233, 240, 253, 261, 272, 277, 291}
+var _ItemType_index = [...]uint16{0, 6, 10, 32, 51, 70, 89, 107, 116, 126, 146, 167, 189, 212, 220, 227, 240, 248, 259, 271, 276, 290}
 
 func (i ItemType) String() string {
        if i < 0 || i >= ItemType(len(_ItemType_index)-1) {
index 20965ac0e91d6d078d77a94b3660a4e756582d06..770f26eb9dc188ee92d23ba6cf62a5354761213e 100644 (file)
@@ -120,6 +120,7 @@ func (l *pageLexer) next() rune {
        runeValue, runeWidth := utf8.DecodeRune(l.input[l.pos:])
        l.width = runeWidth
        l.pos += l.width
+
        return runeValue
 }
 
@@ -137,8 +138,34 @@ func (l *pageLexer) backup() {
 
 // sends an item back to the client.
 func (l *pageLexer) emit(t ItemType) {
+       defer func() {
+               l.start = l.pos
+       }()
+
+       if t == tText {
+               // Identify any trailing whitespace/intendation.
+               // We currently only care about the last one.
+               for i := l.pos - 1; i >= l.start; i-- {
+                       b := l.input[i]
+                       if b != ' ' && b != '\t' && b != '\r' && b != '\n' {
+                               break
+                       }
+                       if i == l.start && b != '\n' {
+                               l.items = append(l.items, Item{tIndentation, l.start, l.input[l.start:l.pos], false})
+                               return
+                       } else if b == '\n' && i < l.pos-1 {
+                               l.items = append(l.items, Item{t, l.start, l.input[l.start : i+1], false})
+                               l.items = append(l.items, Item{tIndentation, i + 1, l.input[i+1 : l.pos], false})
+                               return
+                       } else if b == '\n' && i == l.pos-1 {
+                               break
+                       }
+
+               }
+       }
+
        l.items = append(l.items, Item{t, l.start, l.input[l.start:l.pos], false})
-       l.start = l.pos
+
 }
 
 // sends a string item back to the client.
index bfbedcf26638f22da02b4f87d709c18448cab3c2..67abefc308914a20bdd6d6833d84ade103f1e5fb 100644 (file)
@@ -149,6 +149,11 @@ func (t *Iterator) Backup() {
        t.lastPos--
 }
 
+// Pos returns the current position in the input.
+func (t *Iterator) Pos() int {
+       return t.lastPos
+}
+
 // check for non-error and non-EOF types coming next
 func (t *Iterator) IsValueNext() bool {
        i := t.Peek()
index 54580217ca679f0cfdf352a8207bc29f1b3bf918..ce12975737dc07fe9922c23ba78707d0ce3e68a7 100644 (file)
@@ -51,6 +51,9 @@ var shortCodeLexerTests = []lexerTest{
 
        {"simple with markup", `{{% sc1 %}}`, []Item{tstLeftMD, tstSC1, tstRightMD, tstEOF}},
        {"with spaces", `{{<     sc1     >}}`, []Item{tstLeftNoMD, tstSC1, tstRightNoMD, tstEOF}},
+       {"indented on new line", "Hello\n    {{% sc1 %}}", []Item{nti(tText, "Hello\n"), nti(tIndentation, "    "), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
+       {"indented on new line tab", "Hello\n\t{{% sc1 %}}", []Item{nti(tText, "Hello\n"), nti(tIndentation, "\t"), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
+       {"indented on first line", "    {{% sc1 %}}", []Item{nti(tIndentation, "    "), tstLeftMD, tstSC1, tstRightMD, tstEOF}},
        {"mismatched rightDelim", `{{< sc1 %}}`, []Item{
                tstLeftNoMD, tstSC1,
                nti(tError, "unrecognized character in shortcode action: U+0025 '%'. Note: Parameters with non-alphanumeric args must be quoted"),