diff options
59 files changed, 2212 insertions, 520 deletions
diff --git a/docs/content/en/functions/RenderString.md b/docs/content/en/functions/RenderString.md new file mode 100644 index 000000000..61f5d6417 --- /dev/null +++ b/docs/content/en/functions/RenderString.md @@ -0,0 +1,37 @@ +--- +title: .RenderString +description: "Renders markup to HTML." +godocref: +date: 2019-12-18 +categories: [functions] +menu: + docs: + parent: "functions" +keywords: [markdown,goldmark,render] +signature: [".RenderString MARKUP"] +--- + +{{< new-in "0.62.0" >}} + +`.RenderString` is a method on `Page` that renders some markup to HTML using the content renderer defined for that page (if not set in the options). + +The method takes an optional map argument with these options: + +display ("inline") +: `inline` or `block`. If `inline` (default), surrounding ´<p></p>` on short snippets will be trimmed. + +markup (defaults to the Page's markup) +: See identifiers in [List of content formats](/content-management/formats/#list-of-content-formats). + +Some examples: + +```go-html-template +{{ $optBlock := dict "display" "block" }} +{{ $optOrg := dict "markup" "org" }} +{{ "**Bold Markdown**" | $p.RenderString }} +{{ "**Bold Block Markdown**" | $p.RenderString $optBlock }} +{{ "/italic org mode/" | $p.RenderString $optOrg }}:REND +``` + + +**Note** that this method is more powerful than the similar [markdownify](functions/markdownify/) function as it also supports [Render Hooks](/getting-started/configuration-markup/#markdown-render-hooks) and it has options to render other markup formats.
\ No newline at end of file diff --git a/docs/content/en/getting-started/configuration-markup.md b/docs/content/en/getting-started/configuration-markup.md index ff0095024..f254b9012 100644 --- a/docs/content/en/getting-started/configuration-markup.md +++ b/docs/content/en/getting-started/configuration-markup.md @@ -74,3 +74,62 @@ endLevel ordered : Whether or not to generate an ordered list instead of an unordered list. + + +## Markdown Render Hooks + +{{< new-in "0.62.0" >}} + +Note that this is only supported with the [Goldmark](#goldmark) renderer. + +These Render Hooks allow custom templates to render links and images from markdown. + +You can do this by creating templates with base names `render-link` and/or `render-image` inside `layouts/_default`. + +You can define [Output Format](/templates/output-formats) specific templates if needed.[^1] Your `layouts` folder may then look like this: + +```bash +layouts +└── _default + └── markup + ├── render-image.html + ├── render-image.rss.xml + └── render-link.html +``` + +Some use cases for the above: + +* Resolve link references using `.GetPage`. This would make links more portable as you could translate `./my-post.md` (and similar constructs that would work on GitHub) into `/blog/2019/01/01/my-post/` etc. +* Add `target=blank` to external links. +* Resolve (look in the page bundle, inside `/assets` etc.) and [transform](/content-management/image-processing) images. + + +[^1]: It's currently only possible to have one set of render hook templates, e.g. not per `Type` or `Section`. We may consider that in a future version. + +### Render Hook Templates + +Both `render-link` and `render-image` templates will receive this context: + +Page +: The [Page](/variables/page/) being rendered. + +Destination +: The URL. + +Title +: The title attribute. + +Text +: The link text. + +A Markdown example for a inline-style link with title: + +```md +[Text](https://www.gohugo.io "Title") +``` + +A very simple template example given the above: + +{{< code file="layouts/_default/render-link.html" >}} +<a href="{{ .Destination | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}>{{ .Text }}{{ with .Page }} (in page {{ .Title }}){{ end }}"</a> +{{< /code >}} diff --git a/helpers/content.go b/helpers/content.go index 4dc4cd413..1c780fefe 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -25,13 +25,14 @@ import ( "github.com/gohugoio/hugo/common/loggers" + "github.com/spf13/afero" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup" bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/config" - "github.com/spf13/afero" "strings" ) @@ -78,6 +79,7 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero ContentFs: contentFs, Logger: logger, }) + if err != nil { return nil, err } diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go new file mode 100644 index 000000000..aa697220d --- /dev/null +++ b/hugolib/content_render_hooks_test.go @@ -0,0 +1,244 @@ +// Copyright 2019 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" + +func TestRenderHooks(t *testing.T) { + config := ` +baseURL="https://example.org" +workingDir="/mywork" +` + b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running() + b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`) + b.WithTemplatesAdded("shortcodes/myshortcode1.html", `{{ partial "mypartial1" }}`) + b.WithTemplatesAdded("shortcodes/myshortcode2.html", `{{ partial "mypartial2" }}`) + b.WithTemplatesAdded("shortcodes/myshortcode3.html", `SHORT3|`) + b.WithTemplatesAdded("shortcodes/myshortcode4.html", ` +<div class="foo"> +{{ .Inner | markdownify }} +</div> +`) + b.WithTemplatesAdded("shortcodes/myshortcode5.html", ` +Inner Inline: {{ .Inner | .Page.RenderString }} +Inner Block: {{ .Inner | .Page.RenderString (dict "display" "block" ) }} +`) + + b.WithTemplatesAdded("shortcodes/myshortcode6.html", `.Render: {{ .Page.Render "myrender" }}`) + b.WithTemplatesAdded("partials/mypartial1.html", `PARTIAL1`) + b.WithTemplatesAdded("partials/mypartial2.html", `PARTIAL2 {{ partial "mypartial3.html" }}`) + b.WithTemplatesAdded("partials/mypartial3.html", `PARTIAL3`) + b.WithTemplatesAdded("partials/mypartial4.html", `PARTIAL4`) + b.WithTemplatesAdded("customview/myrender.html", `myrender: {{ .Title }}|P4: {{ partial "mypartial4" }}`) + b.WithTemplatesAdded("_default/_markup/render-link.html", `{{ with .Page }}{{ .Title }}{{ end }}|{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`) + b.WithTemplatesAdded("docs/_markup/render-link.html", `Link docs section: {{ .Text | safeHTML }}|END`) + b.WithTemplatesAdded("_default/_markup/render-image.html", `IMAGE: {{ .Page.Title }}||{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`) + + b.WithContent("customview/p1.md", `--- +title: Custom View +--- + +{{< myshortcode6 >}} + + `, "blog/p1.md", `--- +title: Cool Page +--- + +[First Link](https://www.google.com "Google's Homepage") + +{{< myshortcode3 >}} + +[Second Link](https://www.google.com "Google's Homepage") + +Image: + +![Drag Racing](/images/Dragster.jpg "image title") + + +`, "blog/p2.md", `--- +title: Cool Page2 +layout: mylayout +--- + +{{< myshortcode1 >}} + +[Some Text](https://www.google.com "Google's Homepage") + + + +`, "blog/p3.md", `--- +title: Cool Page3 +--- + +{{< myshortcode2 >}} + + +`, "docs/docs1.md", `--- +title: Docs 1 +--- + + +[Docs 1](https://www.google.com "Google's Homepage") + + +`, "blog/p4.md", `--- +title: Cool Page With Image +--- + +Image: + +![Drag Racing](/images/Dragster.jpg "image title") + + +`, "blog/p5.md", `--- +title: Cool Page With Markdownify +--- + +{{< myshortcode4 >}} +Inner Link: [Inner Link](https://www.google.com "Google's Homepage") +{{< /myshortcode4 >}} + +`, "blog/p6.md", `--- +title: With RenderString +--- + +{{< myshortcode5 >}}Inner Link: [Inner Link](https://www.gohugo.io "Hugo's Homepage"){{< /myshortcode5 >}} + +`) + b.Build(BuildCfg{}) + b.AssertFileContent("public/blog/p1/index.html", ` +<p>Cool Page|https://www.google.com|Title: Google's Homepage|Text: First Link|END</p> +Text: Second +SHORT3| +<p>IMAGE: Cool Page||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p> +`) + + b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4`) + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`) + // We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`) + b.AssertFileContent("public/blog/p4/index.html", `<p>IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>`) + // The regular markdownify func currently gets regular links. + b.AssertFileContent("public/blog/p5/index.html", "Inner Link: <a href=\"https://www.google.com\" title=\"Google's Homepage\">Inner Link</a>\n</div>") + + b.AssertFileContent("public/blog/p6/index.html", + "Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END", + "Inner Block: <p>Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END</p>", + ) + + b.EditFiles( + "layouts/_default/_markup/render-link.html", `EDITED: {{ .Destination | safeURL }}|`, + "layouts/_default/_markup/render-image.html", `IMAGE EDITED: {{ .Destination | safeURL }}|`, + "layouts/docs/_markup/render-link.html", `DOCS EDITED: {{ .Destination | safeURL }}|`, + "layouts/partials/mypartial1.html", `PARTIAL1_EDITED`, + "layouts/partials/mypartial3.html", `PARTIAL3_EDITED`, + "layouts/partials/mypartial4.html", `PARTIAL4_EDITED`, + "layouts/shortcodes/myshortcode3.html", `SHORT3_EDITED|`, + ) + + b.Build(BuildCfg{}) + b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4_EDITED`) + b.AssertFileContent("public/blog/p1/index.html", `<p>EDITED: https://www.google.com|</p>`, "SHORT3_EDITED|") + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL1_EDITED`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3_EDITED`) + // We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `DOCS EDITED: https://www.google.com|</p>`) + b.AssertFileContent("public/blog/p4/index.html", `IMAGE EDITED: /images/Dragster.jpg|`) + b.AssertFileContent("public/blog/p6/index.html", "<p>Inner Link: EDITED: https://www.gohugo.io|</p>") + +} + +func TestRenderHooksRSS(t *testing.T) { + + b := newTestSitesBuilder(t) + + b.WithTemplates("index.html", ` +{{ $p := site.GetPage "p1.md" }} + +P1: {{ $p.Content }} + + `, "index.xml", ` + +{{ $p2 := site.GetPage "p2.md" }} +{{ $p3 := site.GetPage "p3.md" }} + +P2: {{ $p2.Content }} +P3: {{ $p3.Content }} + + + `, + "_default/_markup/render-link.html", `html-link: {{ .Destination | safeURL }}|`, + "_default/_markup/render-link.rss.xml", `xml-link: {{ .Destination | safeURL }}|`, + ) + + b.WithContent("p1.md", `--- +title: "p1" +--- +P1. [I'm an inline-style link](https://www.gohugo.io) + + +`, "p2.md", `--- +title: "p2" +--- +P1. [I'm an inline-style link](https://www.bep.is) + + +`, + "p3.md", `--- +title: "p2" +outputs: ["rss"] +--- +P3. [I'm an inline-style link](https://www.example.org) + +`, + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", "P1: <p>P1. html-link: https://www.gohugo.io|</p>") + b.AssertFileContent("public/index.xml", ` +P2: <p>P1. xml-link: https://www.bep.is|</p> +P3: <p>P3. xml-link: https://www.example.org|</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 + +`) + + 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 +`) + +} diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index de6baa130..cdc39ce61 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -126,10 +126,28 @@ type SourceFilesystems struct { StaticDirs []hugofs.FileMetaInfo } +// FileSystems returns the FileSystems relevant for the change detection +// in server mode. +// Note: This does currently not return any static fs. +func (s *SourceFilesystems) FileSystems() []*SourceFilesystem { + return []*SourceFilesystem{ + s.Content, + s.Data, + s.I18n, + s.Layouts, + s.Archetypes, + // TODO(bep) static + } + +} + // A SourceFilesystem holds the filesystem for a given source type in Hugo (data, // i18n, layouts, static) and additional metadata to be able to use that filesystem // in server mode. type SourceFilesystem struct { + // Name matches one in files.ComponentFolders + Name string + // This is a virtual composite filesystem. It expects path relative to a context. Fs afero.Fs @@ -275,6 +293,19 @@ func (d *SourceFilesystem) Contains(filename string) bool { return false } +// Path returns the relative path to the given filename if it is a member of +// of the current filesystem, an empty string if not. +func (d *SourceFilesystem) Path(filename string) string { + for _, dir := range d.Dirs { + meta := dir.Meta() + if strings.HasPrefix(filename, meta.Filename()) { + p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename()), filePathSeparator) + return p + } + } + return "" +} + // RealDirs gets a list of absolute paths to directories starting from the given // path. func (d *SourceFilesystem) RealDirs(from string) []string { @@ -349,12 +380,14 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *Base return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}} } -func (b *sourceFilesystemsBuilder) newSourceFilesystem(fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem { +func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem { return &SourceFilesystem{ + Name: name, Fs: fs, Dirs: dirs, } } + |