Repository: gohugoio/hugo (language: go)

Issue:
Illegal shortcode state when using escaped quotes in shortcode arguments
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.102.1+extended darwin/arm64 BuildDate=unknown
</pre>

### Does this issue reproduce with the latest release?

This issue happens with the latest release `(v0.102.1)` but did not happen with `v0.101.0`.

### Description

Shortcodes with escaped quotes in arguments fail with `Illegal shortcode state`. It was also reported in Discourse (https://discourse.gohugo.io/t/hugo-v0-102-1-illegal-shortcode-state/40285)

### Steps to reproduce

#### Create default site ✅
<pre>
$ hugo new site quickstart  
Congratulations! Your new Hugo site is created [...]
$ cd quickstart
</pre>

#### Create default index page with a shortcode with quoted string ✅
<pre>
$ hugo new _index.md
Content "/Users/hugo/testhugo/quickstart/content/_index.md" created
$ echo '{{< figure src="/media/spf13.jpg" title="Steve \"Francia\"" >}}' >> content/_index.md
</pre>

#### Build ❌
<pre>
$ hugo -D
hugo v0.102.1+extended darwin/arm64 BuildDate=unknown
Error: Error building site: "/home/test/quickstart/content/_index.md:7:1": Illegal shortcode state
Total in 7 ms
</pre>


Discussion / hints:
Git bisect says 223bf2800488ad5d38854bbb595d789bc35ebe32 is the first bad commit.

With v0.101.0

```
{{< sc foo="\"wibble\"" >}}  --> (.Get "foo") --> "wibble" (with quotes)
```

With 223bf2800488ad5d38854bbb595d789bc35ebe32

```
{{< sc foo="\"wibble\"" >}}  --> (.Get "foo") --> <nil>
```

MRE

```bash
git clone --single-branch -b hugo-github-issue-10236 https://github.com/jmooring/hugo-testing hugo-github-issue-10236
cd hugo-github-issue-10236
hugo
```

Ref: <https://discourse.gohugo.io/t/40285>

Fix patch:
diff --git a/hugolib/page.go b/hugolib/page.go
index 4752d11f163..37bf528c759 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -39,8 +39,6 @@ import (
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/parser/metadecoders"
 
-	"errors"
-
 	"github.com/gohugoio/hugo/parser/pageparser"
 
 	"github.com/gohugoio/hugo/output"
@@ -762,7 +760,7 @@ Loop:
 		case it.IsEOF():
 			break Loop
 		case it.IsError():
-			err := fail(errors.New(it.ValStr(result.Input())), it)
+			err := fail(it.Err, it)
 			currShortcode.err = err
 			return err
 
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 1627acacb9c..b2f42ff1d99 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -497,8 +497,6 @@ func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format
 	return rendered, hasVariants, nil
 }
 
-var errShortCodeIllegalState = errors.New("Illegal shortcode state")
-
 func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error {
 	if s.p != nil {
 		return s.p.parseError(err, input, pos)
@@ -640,7 +638,7 @@ Loop:
 					if params, ok := sc.params.(map[string]any); ok {
 						params[currItem.ValStr(source)] = pt.Next().ValTyped(source)
 					} else {
-						return sc, errShortCodeIllegalState
+						return sc, fmt.Errorf("%s: invalid state: invalid param type %T for shortcode %q, expected a map", errorPrefix, params, sc.name)
 					}
 				}
 			} else {
@@ -654,7 +652,7 @@ Loop:
 						params = append(params, currItem.ValTyped(source))
 						sc.params = params
 					} else {
-						return sc, errShortCodeIllegalState
+						return sc, fmt.Errorf("%s: invalid state: invalid param type %T for shortcode %q, expected a slice", errorPrefix, params, sc.name)
 					}
 				}
 			}
diff --git a/parser/pageparser/pagelexer.go b/parser/pageparser/pagelexer.go
index a7e6b6cd4e7..5f5d147e699 100644
--- a/parser/pageparser/pagelexer.go
+++ b/parser/pageparser/pagelexer.go
@@ -194,7 +194,12 @@ func (l *pageLexer) ignoreEscapesAndEmit(t ItemType, isString bool) {
 			if i > k {
 				segments = append(segments, lowHigh{k, i})
 			}
-			l.append(Item{Type: TypeIgnore, low: i, high: i + w})
+			// See issue #10236.
+			// We don't send the backslash back to the client,
+			// which makes the end parsing simpler.
+			// This means that we cannot render the AST back to be
+			// exactly the same as the input,
+			// but that was also the situation before we introduced the issue in #10236.
 			k = i + w
 		}
 		i += w


Test patch:
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index 3f919096259..ec521729b8b 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -1055,3 +1055,35 @@ title: "p1"
 	`)
 
 }
+
+// Issue 10236.
+func TestShortcodeParamEscapedQuote(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+-- content/p1.md --
+---
+title: "p1"
+---
+
+{{< figure src="/media/spf13.jpg" title="Steve \"Francia\"." >}}
+
+-- layouts/shortcodes/figure.html --
+Title: {{ .Get "title" | safeHTML }}
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			Running:     true,
+			Verbose:     true,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", `Title: Steve "Francia".`)
+
+}
diff --git a/parser/pageparser/pageparser_shortcode_test.go b/parser/pageparser/pageparser_shortcode_test.go
index a95d55ef362..26d836e32a1 100644
--- a/parser/pageparser/pageparser_shortcode_test.go
+++ b/parser/pageparser/pageparser_shortcode_test.go
@@ -40,7 +40,6 @@ var (
 	tstParamFloat     = nti(tScParam, "3.14")
 	tstVal            = nti(tScParamVal, "Hello World")
 	tstText           = nti(tText, "Hello World")
-	tstIgnoreEscape   = nti(TypeIgnore, "\\")
 )
 
 var shortCodeLexerTests = []lexerTest{
@@ -179,14 +178,14 @@ var shortCodeLexerTests = []lexerTest{
 		"escaped quotes inside nonescaped quotes",
 		`{{< sc1 param1="Hello \"escaped\" World"  >}}`,
 		[]typeText{
-			tstLeftNoMD, tstSC1, tstParam1, tstIgnoreEscape, tstIgnoreEscape, nti(tScParamVal, `Hello "escaped" World`), tstRightNoMD, tstEOF,
+			tstLeftNoMD, tstSC1, tstParam1, nti(tScParamVal, `Hello "escaped" World`), tstRightNoMD, tstEOF,
 		},
 	},
 	{
 		"escaped quotes inside nonescaped quotes in positional param",
 		`{{< sc1 "Hello \"escaped\" World"  >}}`,
 		[]typeText{
-			tstLeftNoMD, tstSC1, tstIgnoreEscape, tstIgnoreEscape, nti(tScParam, `Hello "escaped" World`), tstRightNoMD, tstEOF,
+			tstLeftNoMD, tstSC1, nti(tScParam, `Hello "escaped" World`), tstRightNoMD, tstEOF,
 		},
 	},
 	{"escaped raw string, named param", `{{< sc1 param1=` + `\` + "`" + "Hello World" + `\` + "`" + ` >}}`, []typeText{