hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit 9e904d756be02ca30e4cd9abb1eae8ba01f9c8af
parent d2cfaede5be420c7d8b701d97b98bc61b87e46d5
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Sun, 29 May 2022 16:41:57 +0200

Make .RenderString render shortcodes

Fixes #6703

Diffstat:
Mcommon/text/transform.go | 5+++--
Mcommon/text/transform_test.go | 15+++++++++++++++
Mhugolib/content_map_page.go | 2+-
Mhugolib/content_render_hooks_test.go | 70----------------------------------------------------------------------
Mhugolib/page.go | 43++++++++++++++++++++++++++++++-------------
Mhugolib/page__content.go | 6+++---
Mhugolib/page__per_output.go | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Ahugolib/renderstring_test.go | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhugolib/shortcode.go | 29++++++++++++++++++++++++++---
Mhugolib/shortcode_test.go | 8+-------
10 files changed, 315 insertions(+), 111 deletions(-)
diff --git a/common/text/transform.go b/common/text/transform.go
@@ -64,11 +64,12 @@ func Puts(s string) string {
 
 // 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")
+	high := strings.IndexRune(s, '\n')
 	for high != -1 {
 		fn(s[:high+1])
 		s = s[high+1:]
-		high = strings.Index(s, "\n")
+
+		high = strings.IndexRune(s, '\n')
 	}
 
 	if s != "" {
diff --git a/common/text/transform_test.go b/common/text/transform_test.go
@@ -59,3 +59,18 @@ line 3`
 	c.Assert(collected, qt.DeepEquals, []string{"line 1\n", "line 2\n", "\n", "line 3"})
 
 }
+
+func BenchmarkVisitLinesAfter(b *testing.B) {
+	const lines = `line 1
+	line 2
+	
+	line 3`
+
+	for i := 0; i < b.N; i++ {
+		VisitLinesAfter(lines, func(s string) {
+
+		})
+
+	}
+
+}
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
@@ -163,7 +163,7 @@ func (m *pageMap) newPageFromContentNode(n *contentNode, parentBucket *pagesMapB
 		},
 	}
 
-	ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil)
+	ps.shortcodeState = newShortcodeHandler(ps, ps.s)
 
 	if err := ps.mapContent(parentBucket, metaProvider); err != nil {
 		return nil, ps.wrapError(err)
diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
@@ -18,7 +18,6 @@ import (
 	"testing"
 
 	qt "github.com/frankban/quicktest"
-	"github.com/gohugoio/hugo/common/loggers"
 )
 
 func TestRenderHookEditNestedPartial(t *testing.T) {
@@ -428,72 +427,3 @@ Image:
 <p>html-image: image.jpg|Text: Hello<br> Goodbye|Plain: Hello GoodbyeEND</p>
 `)
 }
-
-func TestRenderString(t *testing.T) {
-	b := newTestSitesBuilder(t)
-
-	b.WithTemplates("index.html", `
-{{ $p := site.GetPage "p1.md" }}
-{{ $optBlock := dict "display" "block" }}
-{{ $optOrg := dict "markup" "org" }}
-RSTART:{{ "**Bold Markdown**" | $p.RenderString }}:REND
-RSTART:{{  "**Bold Block Markdown**" | $p.RenderString  $optBlock }}:REND
-RSTART:{{  "/italic org mode/" | $p.RenderString  $optOrg }}:REND
-RSTART:{{ "## Header2" | $p.RenderString }}:REND
-
-
-`, "_default/_markup/render-heading.html", "Hook Heading: {{ .Level }}")
-
-	b.WithContent("p1.md", `---
-title: "p1"
----
-`,
-	)
-
-	b.Build(BuildCfg{})
-
-	b.AssertFileContent("public/index.html", `
-RSTART:<strong>Bold Markdown</strong>:REND
-RSTART:<p><strong>Bold Block Markdown</strong></p>
-RSTART:<em>italic org mode</em>:REND
-RSTART:Hook Heading: 2:REND
-`)
-}
-
-// https://github.com/gohugoio/hugo/issues/6882
-func TestRenderStringOnListPage(t *testing.T) {
-	renderStringTempl := `
-{{ .RenderString "**Hello**" }}
-`
-	b := newTestSitesBuilder(t)
-	b.WithContent("mysection/p1.md", `FOO`)
-	b.WithTemplates(
-		"index.html", renderStringTempl,
-		"_default/list.html", renderStringTempl,
-		"_default/single.html", renderStringTempl,
-	)
-
-	b.Build(BuildCfg{})
-
-	for _, filename := range []string{
-		"index.html",
-		"mysection/index.html",
-		"categories/index.html",
-		"tags/index.html",
-		"mysection/p1/index.html",
-	} {
-		b.AssertFileContent("public/"+filename, `<strong>Hello</strong>`)
-	}
-}
-
-// Issue 9433
-func TestRenderStringOnPageNotBackedByAFile(t *testing.T) {
-	t.Parallel()
-	logger := loggers.NewWarningLogger()
-	b := newTestSitesBuilder(t).WithLogger(logger).WithConfigFile("toml", `
-disableKinds = ["page", "section", "taxonomy", "term"]	
-`)
-	b.WithTemplates("index.html", `{{ .RenderString "**Hello**" }}`).WithContent("p1.md", "")
-	b.BuildE(BuildCfg{})
-	b.Assert(int(logger.LogCounters().WarnCounter.Count()), qt.Equals, 0)
-}
diff --git a/hugolib/page.go b/hugolib/page.go
@@ -336,7 +336,7 @@ func (p *pageState) HasShortcode(name string) bool {
 		return false
 	}
 
-	return p.shortcodeState.nameSet[name]
+	return p.shortcodeState.hasName(name)
 }
 
 func (p *pageState) Site() page.Site {
@@ -610,13 +610,30 @@ func (p *pageState) getContentConverter() converter.Converter {
 }
 
 func (p *pageState) mapContent(bucket *pagesMapBucket, meta *pageMeta) error {
-	s := p.shortcodeState
-
-	rn := &pageContentMap{
+	p.cmap = &pageContentMap{
 		items: make([]any, 0, 20),
 	}
 
-	iter := p.source.parsed.Iterator()
+	return p.mapContentForResult(
+		p.source.parsed,
+		p.shortcodeState,
+		p.cmap,
+		meta.markup,
+		func(m map[string]interface{}) error {
+			return meta.setMetadata(bucket, p, m)
+		},
+	)
+}
+
+func (p *pageState) mapContentForResult(
+	result pageparser.Result,
+	s *shortcodeHandler,
+	rn *pageContentMap,
+	markup string,
+	withFrontMatter func(map[string]any) error,
+) error {
+
+	iter := result.Iterator()
 
 	fail := func(err error, i pageparser.Item) error {
 		if fe, ok := err.(herrors.FileError); ok {
@@ -660,8 +677,10 @@ Loop:
 				}
 			}
 
-			if err := meta.setMetadata(bucket, p, m); err != nil {
-				return err
+			if withFrontMatter != nil {
+				if err := withFrontMatter(m); err != nil {
+					return err
+				}
 			}
 
 			frontMatterSet = true
@@ -697,7 +716,7 @@ Loop:
 			p.source.posBodyStart = posBody
 			p.source.hasSummaryDivider = true
 
-			if meta.markup != "html" {
+			if markup != "html" {
 				// The content will be rendered by Goldmark or similar,
 				// and we need to track the summary.
 				rn.AddReplacement(internalSummaryDividerPre, it)
@@ -720,7 +739,7 @@ Loop:
 			}
 
 			if currShortcode.name != "" {
-				s.nameSet[currShortcode.name] = true
+				s.addName(currShortcode.name)
 			}
 
 			if currShortcode.params == nil {
@@ -752,16 +771,14 @@ Loop:
 		}
 	}
 
-	if !frontMatterSet {
+	if !frontMatterSet && withFrontMatter != nil {
 		// Page content without front matter. Assign default front matter from
 		// cascades etc.
-		if err := meta.setMetadata(bucket, p, nil); err != nil {
+		if err := withFrontMatter(nil); err != nil {
 			return err
 		}
 	}
 
-	p.cmap = rn
-
 	return nil
 }
 
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
@@ -39,12 +39,12 @@ type pageContent struct {
 }
 
 // returns the content to be processed by Goldmark or similar.
-func (p pageContent) contentToRender(renderedShortcodes map[string]string) []byte {
-	source := p.source.parsed.Input()
+func (p pageContent) contentToRender(parsed pageparser.Result, pm *pageContentMap, renderedShortcodes map[string]string) []byte {
+	source := parsed.Input()
 
 	c := make([]byte, 0, len(source)+(len(source)/10))
 
-	for _, it := range p.cmap.items {
+	for _, it := range pm.items {
 		switch v := it.(type) {
 		case pageparser.Item:
 			c = append(c, source[v.Pos:v.Pos+len(v.Val)]...)
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
@@ -25,9 +25,11 @@ import (
 
 	"errors"
 
+	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/gohugoio/hugo/common/text"
 	"github.com/gohugoio/hugo/common/types/hstring"
 	"github.com/gohugoio/hugo/identity"
+	"github.com/gohugoio/hugo/parser/pageparser"
 	"github.com/mitchellh/mapstructure"
 	"github.com/spf13/cast"
 
@@ -117,7 +119,7 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
 			p.pageOutputTemplateVariationsState.Store(2)
 		}
 
-		cp.workContent = p.contentToRender(cp.contentPlaceholders)
+		cp.workContent = p.contentToRender(p.source.parsed, p.cmap, cp.contentPlaceholders)
 
 		isHTML := cp.p.m.markup == "html"
 
@@ -332,11 +334,12 @@ func (p *pageContentOutput) WordCount() int {
 }
 
 func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) {
+	defer herrors.Recover()
 	if len(args) < 1 || len(args) > 2 {
 		return "", errors.New("want 1 or 2 arguments")
 	}
 
-	var s string
+	var contentToRender string
 	opts := defaultRenderStringOpts
 	sidx := 1
 
@@ -353,16 +356,16 @@ func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) {
 		}
 	}
 
-	contentToRender := args[sidx]
+	contentToRenderv := args[sidx]
 
-	if _, ok := contentToRender.(hstring.RenderedString); ok {
+	if _, ok := contentToRenderv.(hstring.RenderedString); ok {
 		// This content is already rendered, this is potentially
 		// a infinite recursion.
 		return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
 	}
 
 	var err error
-	s, err = cast.ToStringE(contentToRender)
+	contentToRender, err = cast.ToStringE(contentToRenderv)
 	if err != nil {
 		return "", err
 	}
@@ -381,20 +384,79 @@ func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) {
 		}
 	}
 
-	c, err := p.renderContentWithConverter(conv, []byte(s), false)
-	if err != nil {
-		return "", p.p.wrapError(err)
-	}
+	var rendered []byte
+
+	if strings.Contains(contentToRender, "{{") {
+		// Probably a shortcode.
+		parsed, err := pageparser.ParseMain(strings.NewReader(contentToRender), pageparser.Config{})
+		if err != nil {
+			return "", err
+		}
+		pm := &pageContentMap{
+			items: make([]any, 0, 20),
+		}
+		s := newShortcodeHandler(p.p, p.p.s)
+
+		if err := p.p.mapContentForResult(
+			parsed,
+			s,
+			pm,
+			opts.Markup,
+			nil,
+		); err != nil {
+			return "", err
+		}
+
+		placeholders, hasShortcodeVariants, err := s.renderShortcodesForPage(p.p, p.f)
+		if err != nil {
+			return "", err
+		}
+
+		if hasShortcodeVariants {
+			p.p.pageOutputTemplateVariationsState.Store(2)
+		}
+
+		b, err := p.renderContentWithConverter(conv, p.p.contentToRender(parsed, pm, placeholders), false)
+		if err != nil {
+			return "", p.p.wrapError(err)
+		}
+		rendered = b.Bytes()
 
-	b := c.Bytes()
+		if p.placeholdersEnabled {
+			// ToC was accessed via .Page.TableOfContents in the shortcode,
+			// at a time when the ToC wasn't ready.
+			if _, err := p.p.Content(); err != nil {
+				return "", err
+			}
+			placeholders[tocShortcodePlaceholder] = string(p.tableOfContents)
+		}
+
+		if pm.hasNonMarkdownShortcode || p.placeholdersEnabled {
+			rendered, err = replaceShortcodeTokens(rendered, placeholders)
+			if err != nil {
+				return "", err
+			}
+		}
+
+		// We need a consolidated view in $page.HasShortcode
+		p.p.shortcodeState.transferNames(s)
+
+	} else {
+		c, err := p.renderContentWithConverter(conv, []byte(contentToRender), false)
+		if err != nil {
+			return "", p.p.wrapError(err)
+		}
+
+		rendered = c.Bytes()
+	}
 
 	if opts.Display == "inline" {
 		// We may have to rethink this in the future when we get other
 		// renderers.
-		b = p.p.s.ContentSpec.TrimShortHTML(b)
+		rendered = p.p.s.ContentSpec.TrimShortHTML(rendered)
 	}
 
-	return template.HTML(string(b)), nil
+	return template.HTML(string(rendered)), nil
 }
 
 func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
diff --git a/hugolib/renderstring_test.go b/hugolib/renderstring_test.go
@@ -0,0 +1,162 @@
+// 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 requiredF 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 hugolib
+
+import (
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/common/loggers"
+)
+
+func TestRenderString(t *testing.T) {
+	b := newTestSitesBuilder(t)
+
+	b.WithTemplates("index.html", `
+{{ $p := site.GetPage "p1.md" }}
+{{ $optBlock := dict "display" "block" }}
+{{ $optOrg := dict "markup" "org" }}
+RSTART:{{ "**Bold Markdown**" | $p.RenderString }}:REND
+RSTART:{{  "**Bold Block Markdown**" | $p.RenderString  $optBlock }}:REND
+RSTART:{{  "/italic org mode/" | $p.RenderString  $optOrg }}:REND
+RSTART:{{ "## Header2" | $p.RenderString }}:REND
+
+
+`, "_default/_markup/render-heading.html", "Hook Heading: {{ .Level }}")
+
+	b.WithContent("p1.md", `---
+title: "p1"
+---
+`,
+	)
+
+	b.Build(BuildCfg{})
+
+	b.AssertFileContent("public/index.html", `
+RSTART:<strong>Bold Markdown</strong>:REND
+RSTART:<p><strong>Bold Block Markdown</strong></p>
+RSTART:<em>italic org mode</em>:REND
+RSTART:Hook Heading: 2:REND
+`)
+}
+
+// https://github.com/gohugoio/hugo/issues/6882
+func TestRenderStringOnListPage(t *testing.T) {
+	renderStringTempl := `
+{{ .RenderString "**Hello**" }}
+`
+	b := newTestSitesBuilder(t)
+	b.WithContent("mysection/p1.md", `FOO`)
+	b.WithTemplates(
+		"index.html", renderStringTempl,
+		"_default/list.html", renderStringTempl,
+		"_default/single.html", renderStringTempl,
+	)
+
+	b.Build(BuildCfg{})
+
+	for _, filename := range []string{
+		"index.html",
+		"mysection/index.html",
+		"categories/index.html",
+		"tags/index.html",
+		"mysection/p1/index.html",
+	} {
+		b.AssertFileContent("public/"+filename, `<strong>Hello</strong>`)
+	}
+}
+
+// Issue 9433
+func TestRenderStringOnPageNotBackedByAFile(t *testing.T) {
+	t.Parallel()
+	logger := loggers.NewWarningLogger()
+	b := newTestSitesBuilder(t).WithLogger(logger).WithConfigFile("toml", `
+disableKinds = ["page", "section", "taxonomy", "term"]	
+`)
+	b.WithTemplates("index.html", `{{ .RenderString "**Hello**" }}`).WithContent("p1.md", "")
+	b.BuildE(BuildCfg{})
+	b.Assert(int(logger.LogCounters().WarnCounter.Count()), qt.Equals, 0)
+}
+
+func TestRenderStringWithShortcode(t *testing.T) {
+	t.Parallel()
+
+	filesTemplate := `
+-- config.toml --
+title = "Hugo Rocks!"
+enableInlineShortcodes = true
+-- content/p1/index.md --
+---
+title: "P1"
+---
+## First
+-- 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/shortcodes/myhthml.html --
+Title: {{ .Page.Title }}
+TableOfContents: {{ .Page.TableOfContents }}
+Page Type: {{ printf "%T" .Page }}
+-- layouts/_default/single.html --
+{{ .RenderString "Markdown: {{% mark2 %}}|HTML: {{< myhthml >}}|Inline: {{< foo.inline >}}{{ site.Title }}{{< /foo.inline >}}|" }}
+HasShortcode: mark2:{{ .HasShortcode "mark2" }}:true
+HasShortcode: foo:{{ .HasShortcode "foo" }}:false
+
+`
+
+	t.Run("Basic", func(t *testing.T) {
+
+		b := NewIntegrationTestBuilder(
+			IntegrationTestConfig{
+				T:           t,
+				TxtarString: filesTemplate,
+			},
+		).Build()
+
+		b.AssertFileContent("public/p1/index.html",
+			"<p>Markdown: 1. Item Mark2 1</p>\n<ol>\n<li>Item Mark2 2\n<ol>\n<li>Item Mark2 2-1</li>\n</ol>\n</li>\n<li>Item Mark2 3|",
+			"<a href=\"#first\">First</a>", // ToC
+			`
+HTML: Title: P1
+Inline: Hugo Rocks!
+HasShortcode: mark2:true:true
+HasShortcode: foo:false:false
+Page Type: *hugolib.pageForShortcode`,
+		)
+
+	})
+
+	t.Run("Edit shortcode", func(t *testing.T) {
+
+		b := NewIntegrationTestBuilder(
+			IntegrationTestConfig{
+				T:           t,
+				TxtarString: filesTemplate,
+				Running:     true,
+			},
+		).Build()
+
+		b.EditFiles("layouts/shortcodes/myhthml.html", "Edit shortcode").Build()
+
+		b.AssertFileContent("public/p1/index.html",
+			`Edit shortcode`,
+		)
+
+	})
+
+}
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
@@ -250,13 +250,14 @@ type shortcodeHandler struct {
 	shortcodes []*shortcode
 
 	// All the shortcode names in this set.
-	nameSet map[string]bool
+	nameSet   map[string]bool
+	nameSetMu sync.RWMutex
 
 	// Configuration
 	enableInlineShortcodes bool
 }
 
-func newShortcodeHandler(p *pageState, s *Site, placeholderFunc func() string) *shortcodeHandler {
+func newShortcodeHandler(p *pageState, s *Site) *shortcodeHandler {
 	sh := &shortcodeHandler{
 		p:                      p,
 		s:                      s,
@@ -423,6 +424,28 @@ func (s *shortcodeHandler) hasShortcodes() bool {
 	return s != nil && len(s.shortcodes) > 0
 }
 
+func (s *shortcodeHandler) addName(name string) {
+	s.nameSetMu.Lock()
+	defer s.nameSetMu.Unlock()
+	s.nameSet[name] = true
+}
+
+func (s *shortcodeHandler) transferNames(in *shortcodeHandler) {
+	s.nameSetMu.Lock()
+	defer s.nameSetMu.Unlock()
+	for k := range in.nameSet {
+		s.nameSet[k] = true
+	}
+
+}
+
+func (s *shortcodeHandler) hasName(name string) bool {
+	s.nameSetMu.RLock()
+	defer s.nameSetMu.RUnlock()
+	_, ok := s.nameSet[name]
+	return ok
+}
+
 func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) {
 	rendered := make(map[string]string)
 
@@ -503,7 +526,7 @@ Loop:
 				nested, err := s.extractShortcode(nestedOrdinal, nextLevel, pt)
 				nestedOrdinal++
 				if nested != nil && nested.name != "" {
-					s.nameSet[nested.name] = true
+					s.addName(nested.name)
 				}
 
 				if err == nil {
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
@@ -107,15 +107,9 @@ title: "Shortcodes Galore!"
 			t.Parallel()
 			c := qt.New(t)
 
-			counter := 0
-			placeholderFunc := func() string {
-				counter++
-				return fmt.Sprintf("HAHA%s-%dHBHB", shortcodePlaceholderPrefix, counter)
-			}
-
 			p, err := pageparser.ParseMain(strings.NewReader(test.input), pageparser.Config{})
 			c.Assert(err, qt.IsNil)
-			handler := newShortcodeHandler(nil, s, placeholderFunc)
+			handler := newShortcodeHandler(nil, s)
 			iter := p.Iterator()
 
 			short, err := handler.extractShortcode(0, 0, iter)