Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-17 15:04:00 +0300
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2022-02-24 20:59:50 +0300
commit08fdca9d9365eaf1e496a12e2af5e18617bd0e66 (patch)
tree6c6942d1b74a4160d93a997860bafd52b92025f5
parent2c20f5bc00b604e72b3b7e401fbdbf9447fe3470 (diff)
Add Markdown diagrams and render hooks for code blocks
You can now create custom hook templates for code blocks, either one for all (`render-codeblock.html`) or for a given code language (e.g. `render-codeblock-go.html`). We also used this new hook to add support for diagrams in Hugo: * Goat (Go ASCII Tool) is built-in and enabled by default; just create a fenced code block with the language `goat` and start draw your Ascii diagrams. * Another popular alternative for diagrams in Markdown, Mermaid (supported by GitHub), can also be implemented with a simple template. See the Hugo documentation for more information. Updates #7765 Closes #9538 Fixes #9553 Fixes #8520 Fixes #6702 Fixes #9558
-rw-r--r--common/hugio/writers.go8
-rw-r--r--docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html8
-rw-r--r--docs/content/en/content-management/diagrams.md217
-rw-r--r--docs/layouts/_default/_markup/render-codeblock-goat.html18
-rw-r--r--docs/layouts/_default/_markup/render-codeblock-mermaid.html4
-rw-r--r--go.mod5
-rw-r--r--go.sum8
-rw-r--r--helpers/content.go12
-rw-r--r--hugolib/content_render_hooks_test.go4
-rw-r--r--hugolib/integrationtest_builder.go4
-rw-r--r--hugolib/language_content_dir_test.go2
-rw-r--r--hugolib/page.go64
-rw-r--r--hugolib/page__new.go5
-rw-r--r--hugolib/page__per_output.go173
-rw-r--r--hugolib/page_test.go5
-rw-r--r--hugolib/pagebundler_test.go4
-rw-r--r--hugolib/site.go16
-rw-r--r--hugolib/site_sections.go10
-rw-r--r--markup/converter/converter.go10
-rw-r--r--markup/converter/hooks/hooks.go100
-rw-r--r--markup/goldmark/codeblocks/integration_test.go115
-rw-r--r--markup/goldmark/codeblocks/render.go159
-rw-r--r--markup/goldmark/codeblocks/transform.go53
-rw-r--r--markup/goldmark/convert.go146
-rw-r--r--markup/goldmark/convert_test.go25
-rw-r--r--markup/goldmark/integration_test.go141
-rw-r--r--markup/goldmark/internal/render/context.go81
-rw-r--r--markup/goldmark/render_hooks.go143
-rw-r--r--markup/goldmark/toc_test.go9
-rw-r--r--markup/highlight/config.go99
-rw-r--r--markup/highlight/highlight.go178
-rw-r--r--markup/internal/attributes/attributes.go219
-rw-r--r--markup/markup.go13
-rw-r--r--markup/org/convert.go3
-rw-r--r--output/layout.go18
-rw-r--r--resources/page/site.go5
-rw-r--r--tpl/cast/init_test.go43
-rw-r--r--tpl/collections/init_test.go43
-rw-r--r--tpl/compare/init.go4
-rw-r--r--tpl/compare/init_test.go42
-rw-r--r--tpl/crypto/init_test.go42
-rw-r--r--tpl/data/init_test.go47
-rw-r--r--tpl/debug/init_test.go44
-rw-r--r--tpl/diagrams/diagrams.go73
-rw-r--r--tpl/diagrams/init.go (renamed from tpl/os/init_test.go)34
-rw-r--r--tpl/encoding/init_test.go42
-rw-r--r--tpl/fmt/init_test.go44
-rw-r--r--tpl/hugo/init_test.go49
-rw-r--r--tpl/images/init_test.go42
-rw-r--r--tpl/inflect/init_test.go43
-rw-r--r--tpl/lang/init_test.go48
-rw-r--r--tpl/math/init_test.go42
-rw-r--r--tpl/os/os.go21
-rw-r--r--tpl/os/os_test.go73
-rw-r--r--tpl/partials/init_test.go46
-rw-r--r--tpl/path/init_test.go43
-rw-r--r--tpl/reflect/init_test.go43
-rw-r--r--tpl/safe/init_test.go43
-rw-r--r--tpl/site/init_test.go49
-rw-r--r--tpl/strings/init_test.go45
-rw-r--r--tpl/templates/init_test.go42
-rw-r--r--tpl/time/init_test.go48
-rw-r--r--tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html1
-rw-r--r--tpl/tplimpl/template.go24
-rw-r--r--tpl/tplimpl/template_funcs.go1
-rw-r--r--tpl/tplimpl/template_funcs_test.go245
-rw-r--r--tpl/tplimpl/template_info_test.go58
-rw-r--r--tpl/transform/init_test.go42
-rw-r--r--tpl/transform/remarshal_test.go15
-rw-r--r--tpl/transform/transform.go41
-rw-r--r--tpl/transform/transform_test.go111
-rw-r--r--tpl/transform/unmarshal_test.go61
-rw-r--r--tpl/urls/init_test.go45
73 files changed, 1882 insertions, 1981 deletions
diff --git a/common/hugio/writers.go b/common/hugio/writers.go
index 82c4dca52..d8be83a40 100644
--- a/common/hugio/writers.go
+++ b/common/hugio/writers.go
@@ -18,6 +18,14 @@ import (
"io/ioutil"
)
+// As implemented by strings.Builder.
+type FlexiWriter interface {
+ io.Writer
+ io.ByteWriter
+ WriteString(s string) (int, error)
+ WriteRune(r rune) (int, error)
+}
+
type multiWriteCloser struct {
io.Writer
closers []io.WriteCloser
diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
index 47019072c..e2886a0b8 100644
--- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
+++ b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html
@@ -66,6 +66,14 @@
{{ block "footer" . }}{{ partialCached "site-footer.html" . }}{{ end }}
+ {{ if .Page.Store.Get "hasMermaid" }}
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
+ <script>
+ mermaid.initialize({ startOnLoad: true });
+ </script>
+{{ end }}
+
+
</body>
</html>
diff --git a/docs/content/en/content-management/diagrams.md b/docs/content/en/content-management/diagrams.md
new file mode 100644
index 000000000..4e3f6164b
--- /dev/null
+++ b/docs/content/en/content-management/diagrams.md
@@ -0,0 +1,217 @@
+---
+title: Diagrams
+date: 2022-02-20
+categories: [content management]
+keywords: [diagrams,drawing]
+menu:
+ docs:
+ parent: "content-management"
+ weight: 22
+weight: 22
+toc: true
+---
+
+
+## Mermaid Diagrams
+
+```mermaid
+sequenceDiagram
+ participant Alice
+ participant Bob
+ Alice->>John: Hello John, how are you?
+ loop Healthcheck
+ John->>John: Fight against hypochondria
+ end
+ Note right of John: Rational thoughts <br/>prevail!
+ John-->>Alice: Great!
+ John->>Bob: How about you?
+ Bob-->>John: Jolly good!
+```
+
+
+
+## Goat Ascii Diagram Examples
+
+### Graphics
+
+```goat
+ .
+ 0 3 P * Eye / ^ /
+ *-------* +y \ +) \ / Reflection
+ 1 /| 2 /| ^ \ \ \ v
+ *-------* | | v0 \ v3 --------*--------
+ | |4 | |7 | *----\-----*
+ | *-----|-* +-----> +x / v X \ .-.<-------- o
+ |/ |/ / / o \ | / | Refraction / \
+ *-------* v / \ +-' / \
+ 5 6 +z v1 *------------------* v2 | o-----o
+ v
+
+```
+
+### Complex
+
+```goat
++-------------------+ ^ .---.
+| A Box |__.--.__ __.--> | .-. | |
+| | '--' v | * |<--- | |
++-------------------+ '-' | |
+ Round *---(-. |
+ .-----------------. .-------. .----------. .-------. | | |
+ | Mixed Rounded | | | / Diagonals \ | | | | | |
+ | & Square Corners | '--. .--' / \ |---+---| '-)-' .--------.
+ '--+------------+-' .--. | '-------+--------' | | | | / Search /
+ | | | | '---. | '-------' | '-+------'
+ |<---------->| | | | v Interior | ^
+ ' <---' '----' .-----------. ---. .--- v |
+ .------------------. Diag line | .-------. +---. \ / . |
+ | if (a > b) +---. .--->| | | | | Curved line \ / / \ |
+ | obj->fcn() | \ / | '-------' |<--' + / \ |
+ '------------------' '--' '--+--------' .--. .--. | .-. +Done?+-'
+ .---+-----. | ^ |\ | | /| .--+ | | \ /
+ | | | Join \|/ | | Curved | \| |/ | | \ | \ /
+ | | +----> o --o-- '-' Vertical '--' '--' '-- '--' + .---.
+ <--+---+-----' | /|\ | | 3 |
+ v not:line 'quotes' .-' '---'
+ .-. .---+--------. / A || B *bold* | ^
+ | | | Not a dot | <---+---<-- A dash--is not a line v |
+ '-' '---------+--' / Nor/is this. ---
+
+```
+
+### Process
+
+```goat
+ .
+ .---------. / \
+ | START | / \ .-+-------+-. ___________
+ '----+----' .-------. A / \ B | |COMPLEX| | / \ .-.
+ | | END |<-----+CHOICE +----->| | | +--->+ PREPARATION +--->| X |
+ v '-------' \ / | |PROCESS| | \___________/ '-'
+ .---------. \ / '-+---+---+-'
+ / INPUT / \ /
+ '-----+---' '
+ | ^
+ v |
+ .-----------. .-----+-----. .-.
+ | PROCESS +---------------->| PROCESS |<------+ X |
+ '-----------' '-----------' '-'
+```
+
+### File tree
+
+Created from https://arthursonzogni.com/Diagon/#Tree
+
+```goat { width=300 color="orange" }
+───Linux─┬─Android
+ ├─Debian─┬─Ubuntu─┬─Lubuntu
+ │ │ ├─Kubuntu
+ │ │ ├─Xubuntu
+ │ │ └─Xubuntu
+ │ └─Mint
+ ├─Centos
+ └─Fedora
+```
+
+
+### Sequence Diagram
+
+https://arthursonzogni.com/Diagon/#Sequence
+
+```goat { class="w-40" }
+┌─────┐ ┌───┐
+│Alice│ │Bob│
+└──┬──┘ └─┬─┘
+ │ │
+ │ Hello Bob! │
+ │───────────>│
+ │ │
+ │Hello Alice!│
+ │<───────────│
+┌──┴──┐ ┌─┴─┐
+│Alice│ │Bob│
+└─────┘ └───┘
+
+```
+
+
+### Flowchart
+
+https://arthursonzogni.com/Diagon/#Flowchart
+
+```goat
+ _________________
+ ╱ ╲ ┌─────┐
+ ╱ DO YOU UNDERSTAND ╲____________________________________________________│GOOD!│
+ ╲ FLOW CHARTS? ╱yes └──┬──┘
+ ╲_________________╱ │
+ │no │
+ _________▽_________ ______________________ │
+ ╱ ╲ ╱ ╲ ┌────┐ │
+╱ OKAY, YOU SEE THE ╲________________╱ ... AND YOU CAN SEE ╲___│GOOD│ │
+╲ LINE LABELED 'YES'? ╱yes ╲ THE ONES LABELED 'NO'? ╱yes└──┬─┘ │
+ ╲___________________╱ ╲______________________╱ │ │
+ │no │no │ │
+ ________▽_________ _________▽__________ │ │
+ ╱ ╲ ┌───────────┐ ╱ ╲ │ │
+ ╱ BUT YOU SEE THE ╲___│WAIT, WHAT?│ ╱ BUT YOU JUST ╲___ │ │
+ ╲ ONES LABELED 'NO'? ╱yes└───────────┘ ╲ FOLLOWED THEM TWICE? ╱yes│ │ │
+ ╲__________________╱ ╲____________________╱ │ │ │
+ │no │no │ │ │
+ ┌───▽───┐ │ │ │ │
+ │LISTEN.│ └───────┬───────┘ │ │
+ └───┬───┘ ┌──────▽─────┐ │ │
+ ┌─────▽────┐ │(THAT WASN'T│ │ │
+ │I HATE YOU│ │A QUESTION) │ │ │
+ └──────────┘ └──────┬─────┘ │ │
+ ┌────▽───┐ │ │
+ │SCREW IT│ │ │
+ └────┬───┘ │ │
+ └─────┬─────┘ │
+ │ │
+ └─────┬─────┘
+ ┌───────▽──────┐
+ │LET'S GO DRING│
+ └───────┬──────┘
+ ┌─────────▽─────────┐
+ │HEY, I SHOULD TRY │
+ │INSTALLING FREEBSD!│
+ └───────────────────┘
+
+```
+
+
+### Table
+
+https://arthursonzogni.com/Diagon/#Table
+
+```goat { class="w-80 dark-blue" }
+┌────────────────────────────────────────────────┐
+│ │
+├────────────────────────────────────────────────┤
+│SYNTAX = { PRODUCTION } . │
+├────────────────────────────────────────────────┤
+│PRODUCTION = IDENTIFIER "=" EXPRESSION "." . │
+├────────────────────────────────────────────────┤
+│EXPRESSION = TERM { "|" TERM } . │
+├────────────────────────────────────────────────┤
+│TERM = FACTOR { FACTOR } . │
+├────────────────────────────────────────────────┤
+│FACTOR = IDENTIFIER │
+├────────────────────────────────────────────────┤
+│ | LITERAL │
+├────────────────────────────────────────────────┤
+│ | "[" EXPRESSION "]" │
+├────────────────────────────────────────────────┤
+│ | "(" EXPRESSION ")" │
+├────────────────────────────────────────────────┤
+│ | "{" EXPRESSION "}" . │
+├────────────────────────────────────────────────┤
+│IDENTIFIER = letter { letter } . │
+├────────────────────────────────────────────────┤
+│LITERAL = """" character { character } """" .│
+└────────────────────────────────────────────────┘
+```
+
+
+
diff --git a/docs/layouts/_default/_markup/render-codeblock-goat.html b/docs/layouts/_default/_markup/render-codeblock-goat.html
new file mode 100644
index 000000000..b1e57e94a
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-codeblock-goat.html
@@ -0,0 +1,18 @@
+{{ $width := .Attributes.width }}
+{{ $height := .Attributes.height }}
+{{ $class := .Attributes.class | default "" }}
+<div class="goat svg-container {{ $class }}">
+ {{ with diagrams.Goat .Code }}
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ font-family="Menlo,Lucida Console,monospace"
+ {{ if or $width $height }}
+ {{ with $width }}width="{{ . }}"{{ end }}
+ {{ with $height }}height="{{ . }}"{{ end }}
+ {{ else }}
+ viewBox="0 0 {{ .Width }} {{ .Height }}"
+ {{ end }}>
+ {{ .Body }}
+ </svg>
+ {{ end }}
+</div>
diff --git a/docs/layouts/_default/_markup/render-codeblock-mermaid.html b/docs/layouts/_default/_markup/render-codeblock-mermaid.html
new file mode 100644
index 000000000..15e4fdfbb
--- /dev/null
+++ b/docs/layouts/_default/_markup/render-codeblock-mermaid.html
@@ -0,0 +1,4 @@
+<div class="mermaid">
+ {{- .Code | safeHTML }}
+</div>
+{{ .Page.Store.Set "hasMermaid" true }}
diff --git a/go.mod b/go.mod
index 8227c2c67..71bdd5607 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go v1.43.5
github.com/bep/debounce v1.2.0
github.com/bep/gitmap v1.1.2
+ github.com/bep/goat v0.5.0
github.com/bep/godartsass v0.12.0
github.com/bep/golibsass v1.0.0
github.com/bep/gowebp v0.1.0
@@ -19,7 +20,7 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/evanw/esbuild v0.14.22
github.com/fortytw2/leaktest v1.3.0
- github.com/frankban/quicktest v1.14.0
+ github.com/frankban/quicktest v1.14.2
github.com/fsnotify/fsnotify v1.5.1
github.com/getkin/kin-openapi v0.85.0
github.com/ghodss/yaml v1.0.0
@@ -57,7 +58,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/tdewolff/minify/v2 v2.9.29
github.com/yuin/goldmark v1.4.7
- github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
+ go.uber.org/atomic v1.9.0
gocloud.dev v0.20.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
diff --git a/go.sum b/go.sum
index 01e8ff60c..4ddc55855 100644
--- a/go.sum
+++ b/go.sum
@@ -144,6 +144,10 @@ github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840=
github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY=
+github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e h1:On3hMv9ffG+0fgPIjKPXiFu5QVS9jM1Vzr5/ghmSLy4=
+github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
+github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
+github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass v0.12.0 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E=
github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4=
github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw=
@@ -239,6 +243,8 @@ github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
+github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns=
+github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
@@ -623,6 +629,8 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8=
diff --git a/helpers/content.go b/helpers/content.go
index 157f75079..3e674bca4 100644
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -30,6 +30,7 @@ import (
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup"
@@ -47,8 +48,8 @@ var (
// ContentSpec provides functionality to render markdown content.
type ContentSpec struct {
Converters markup.ConverterProvider
- MardownConverter converter.Converter // Markdown converter with no document context
anchorNameSanitizer converter.AnchorNameSanitizer
+ getRenderer func(t hooks.RendererType, id interface{}) interface{}
// SummaryLength is the length of the summary that Hugo extracts from a content.
summaryLength int
@@ -88,7 +89,6 @@ func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.
if err != nil {
return nil, err
}
- spec.MardownConverter = conv
if as, ok := conv.(converter.AnchorNameSanitizer); ok {
spec.anchorNameSanitizer = as
} else {
@@ -192,14 +192,6 @@ func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
return
}
-func (c *ContentSpec) RenderMarkdown(src []byte) ([]byte, error) {
- b, err := c.MardownConverter.Convert(converter.RenderContext{Src: src})
- if err != nil {
- return nil, err
- }
- return b.Bytes(), nil
-}
-
func (c *ContentSpec) SanitizeAnchorName(s string) string {
return c.anchorNameSanitizer.SanitizeAnchorName(s)
}
diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
index edfeaa82a..33ebe1f41 100644
--- a/hugolib/content_render_hooks_test.go
+++ b/hugolib/content_render_hooks_test.go
@@ -231,8 +231,8 @@ SHORT3|
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>")
+ // markdownify
+ b.AssertFileContent("public/blog/p5/index.html", "Inner Link: |https://www.google.com|Title: Google's Homepage|Text: Inner Link|END")
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",
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 7ec7a1503..ed68783a1 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -125,7 +125,7 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
if match == "" || strings.HasPrefix(match, "#") {
continue
}
- s.Assert(content, qt.Contains, match, qt.Commentf(content))
+ s.Assert(content, qt.Contains, match, qt.Commentf(m))
}
}
}
@@ -164,7 +164,7 @@ func (s *IntegrationTestBuilder) AssertRenderCountPage(count int) {
func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
s.Helper()
_, err := s.BuildE()
- if s.Cfg.Verbose {
+ if s.Cfg.Verbose || err != nil {
fmt.Println(s.logBuff.String())
}
s.Assert(err, qt.IsNil)
diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go
index 117fdfb14..9a7a78e7e 100644
--- a/hugolib/language_content_dir_test.go
+++ b/hugolib/language_content_dir_test.go
@@ -314,7 +314,7 @@ Content.
nnSect := nnSite.getPage(page.KindSection, "sect")
c.Assert(nnSect, qt.Not(qt.IsNil))
c.Assert(len(nnSect.Pages()), qt.Equals, 12)
- nnHome, _ := nnSite.Info.Home()
+ nnHome := nnSite.Info.Home()
c.Assert(nnHome.RelPermalink(), qt.Equals, "/nn/")
}
diff --git a/hugolib/page.go b/hugolib/page.go
index 83b654cc0..7101af814 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -22,6 +22,8 @@ import (
"sort"
"strings"
+ "go.uber.org/atomic"
+
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter"
@@ -47,7 +49,6 @@ import (
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/text"
- "github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
@@ -118,6 +119,9 @@ type pageState struct {
// formats (for all sites).
pageOutputs []*pageOutput
+ // Used to determine if we can reuse content across output formats.
+ pageOutputTemplateVariationsState *atomic.Uint32
+
// This will be shifted out when we start to render a new output format.
*pageOutput
@@ -125,6 +129,10 @@ type pageState struct {
*pageCommon
}
+func (p *pageState) reusePageOutputContent() bool {
+ return p.pageOutputTemplateVariationsState.Load() == 1
+}
+
func (p *pageState) Err() error {
return nil
}
@@ -394,56 +402,6 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil
}
-func (p *pageState) createRenderHooks(f output.Format) (hooks.Renderers, error) {
- layoutDescriptor := p.getLayoutDescriptor()
- layoutDescriptor.RenderingHook = true
- layoutDescriptor.LayoutOverride = false
- layoutDescriptor.Layout = ""
-
- var renderers hooks.Renderers
-
- layoutDescriptor.Kind = "render-link"
- templ, templFound, err := p.s.Tmpl().LookupLayout(layoutDescriptor, f)
- if err != nil {
- return renderers, err
- }
- if templFound {
- renderers.LinkRenderer = hookRenderer{
- templateHandler: p.s.Tmpl(),
- SearchProvider: templ.(identity.SearchProvider),
- templ: templ,
- }
- }
-
- layoutDescriptor.Kind = "render-image"
- templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
- if err != nil {
- return renderers, err
- }
- if templFound {
- renderers.ImageRenderer = hookRenderer{
- templateHandler: p.s.Tmpl(),
- SearchProvider: templ.(identity.SearchProvider),
- templ: templ,
- }
- }
-
- layoutDescriptor.Kind = "render-heading"
- templ, templFound, err = p.s.Tmpl().LookupLayout(layoutDescriptor, f)
- if err != nil {
- return renderers, err
- }
- if templFound {
- renderers.HeadingRenderer = hookRenderer{
- templateHandler: p.s.Tmpl(),
- SearchProvider: templ.(identity.SearchProvider),
- templ: templ,
- }
- }
-
- return renderers, nil
-}
-
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
p.layoutDescriptorInit.Do(func() {
var section string
@@ -867,7 +825,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if isRenderingSite {
cp := p.pageOutput.cp
- if cp == nil {
+ if cp == nil && p.reusePageOutputContent() {
// Look for content to reuse.
for i := 0; i < len(p.pageOutputs); i++ {
if i == idx {
@@ -875,7 +833,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
}
po := p.pageOutputs[i]
- if po.cp != nil && po.cp.reuse {
+ if po.cp != nil {
cp = po.cp
break
}
diff --git a/hugolib/page__new.go b/hugolib/page__new.go
index 918477843..b8395d5ca 100644
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -17,6 +17,8 @@ import (
"html/template"
"strings"
+ "go.uber.org/atomic"
+
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/maps"
@@ -36,7 +38,8 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) {
s := metaProvider.s
ps := &pageState{
- pageOutput: nopPageOutput,
+ pageOutput: nopPageOutput,
+ pageOutputTemplateVariationsState: atomic.NewUint32(0),
pageCommon: &pageCommon{
FileProvider: metaProvider,
AuthorProvider: metaProvider,
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index bd4e35a5b..29beb672e 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -32,6 +32,7 @@ import (
"github.com/gohugoio/hugo/markup/converter"
+ "github.com/alecthomas/chroma/lexers"
"github.com/gohugoio/hugo/lazy"
bp "github.com/gohugoio/hugo/bufferpool"
@@ -109,16 +110,8 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
return err
}
- enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
-
- if enableReuse {
- // Reuse this for the other output formats.
- // We may improve on this, but we really want to avoid re-rendering the content
- // to all output formats.
- // The current rule is that if you need output format-aware shortcodes or
- // content rendering hooks, create a output format-specific template, e.g.
- // myshortcode.amp.html.
- cp.enableReuse()
+ if hasShortcodeVariants {
+ p.pageOutputTemplateVariationsState.Store(2)
}
cp.workContent = p.contentToRender(cp.contentPlaceholders)
@@ -199,19 +192,10 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
return nil
}
- // Recursive loops can only happen in content files with template code (shortcodes etc.)
- // Avoid creating new goroutines if we don't have to.
- needTimeout := p.shortcodeState.hasShortcodes() || cp.renderHooks != nil
-
- if needTimeout {
- cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
- return nil, initContent()
- })
- } else {
- cp.initMain = parent.Branch(func() (interface{}, error) {
- return nil, initContent()
- })
- }
+ // There may be recursive loops in shortcodes and render hooks.
+ cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
+ return nil, initContent()
+ })
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
cp.plain = helpers.StripHTML(string(cp.content))
@@ -229,18 +213,14 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
}
type renderHooks struct {
- hooks hooks.Renderers
- init sync.Once
+ getRenderer hooks.GetRendererFunc
+ init sync.Once
}
// pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct {
f output.Format
- // If we can reuse this for other output formats.
- reuse bool
- reuseInit sync.Once
-
p *pageState
// Lazy load dependencies
@@ -250,13 +230,9 @@ type pageContentOutput struct {
placeholdersEnabled bool
placeholdersEnabledInit sync.Once
+ // Renders Markdown hooks.
renderHooks *renderHooks
- // Set if there are more than one output format variant
- renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
-
- // Content state
-
workContent []byte
dependencyTracker identity.Manager // Set in server mode.
@@ -440,55 +416,107 @@ func (p *pageContentOutput) initRenderHooks() error {
return nil
}
- var initErr error
-
p.renderHooks.init.Do(func() {
- ps := p.p
-
- c := ps.getContentConverter()
- if c == nil || !c.Supports(converter.FeatureRenderHooks) {
- return
+ if p.p.pageOutputTemplateVariationsState.Load() == 0 {
+ p.p.pageOutputTemplateVariationsState.Store(1)
}
- h, err := ps.createRenderHooks(p.f)
- if err != nil {
- initErr = err
- return
+ type cacheKey struct {
+ tp hooks.RendererType
+ id interface{}
+ f output.Format
}
- p.renderHooks.hooks = h
-
- if !p.renderHooksHaveVariants || h.IsZero() {
- // Check if there is a different render hooks template
- // for any of the other page output formats.
- // If not, we can reuse this.
- for _, po := range ps.pageOutputs {
- if po.f.Name != p.f.Name {
- h2, err := ps.createRenderHooks(po.f)
- if err != nil {
- initErr = err
- return
- }
- if h2.IsZero() {
- continue
- }
+ renderCache := make(map[cacheKey]interface{})
+ var renderCacheMu sync.Mutex
+
+ p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} {
+ renderCacheMu.Lock()
+ defer renderCacheMu.Unlock()
+
+ key := cacheKey{tp: tp, id: id, f: p.f}
+ if r, ok := renderCache[key]; ok {
+ return r
+ }
- if p.renderHooks.hooks.IsZero() {
- p.renderHooks.hooks = h2
+ layoutDescriptor := p.p.getLayoutDescriptor()
+ layoutDescriptor.RenderingHook = true
+ layoutDescriptor.LayoutOverride = false
+ layoutDescriptor.Layout = ""
+
+ switch tp {
+ case hooks.LinkRendererType:
+ layoutDescriptor.Kind = "render-link"
+ case hooks.ImageRendererType:
+ layoutDescriptor.Kind = "render-image"
+ case hooks.HeadingRendererType:
+ layoutDescriptor.Kind = "render-heading"
+ case hooks.CodeBlockRendererType:
+ layoutDescriptor.Kind = "render-codeblock"
+ if id != nil {
+ lang := id.(string)
+ lexer := lexers.Get(lang)
+ if lexer != nil {
+ layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
+ } else {
+ layoutDescriptor.KindVariants = lang
}
+ }
+ }
- p.renderHooksHaveVariants = !h2.Eq(p.renderHooks.hooks)
+ getHookTemplate := func(f output.Format) (tpl.Template, bool) {
+ templ, found, err := p.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
+ if err != nil {
+ panic(err)
+ }
+ return templ, found
+ }
+
+ templ, found1 := getHookTemplate(p.f)
- if p.renderHooksHaveVariants {
- break
+ if p.p.reusePageOutputContent() {
+ // Check if some of the other output formats would give a different template.
+ for _, f := range p.p.s.renderFormats {
+ if f.Name == p.f.Name {
+ continue
+ }
+ templ2, found2 := getHookTemplate(f)
+ if found2 {
+ if !found1 {
+ templ = templ2
+ found1 = true
+ break
+ }
+
+ if templ != templ2 {
+ p.p.pageOutputTemplateVariationsState.Store(2)
+ break
+ }
}
+ }
+ }
+ if !found1 {
+ if tp == hooks.CodeBlockRendererType {
+ // No user provided tempplate for code blocks, so we use the native Go code version -- which is also faster.
+ r := p.p.s.ContentSpec.Converters.GetHighlighter()
+ renderCache[key] = r
+ return r
}
+ return nil
}
+
+ r := hookRendererTemplate{
+ templateHandler: p.p.s.Tmpl(),
+ SearchProvider: templ.(identity.SearchProvider),
+ templ: templ,
+ }
+ renderCache[key] = r
+ return r
}
})
- return initErr
+ return nil
}
func (p *pageContentOutput) setAutoSummary() error {
@@ -512,6 +540,9 @@ func (p *pageContentOutput) setAutoSummary() error {
}
func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
+ if err := cp.initRenderHooks(); err != nil {
+ return nil, err
+ }
c := cp.p.getContentConverter()
return cp.renderContentWithConverter(c, content, renderTOC)
}
@@ -521,7 +552,7 @@ func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, c
converter.RenderContext{
Src: content,
RenderTOC: renderTOC,
- RenderHooks: cp.renderHooks.hooks,
+ GetRenderer: cp.renderHooks.getRenderer,
})
if err == nil {
@@ -570,12 +601,6 @@ func (p *pageContentOutput) enablePlaceholders() {
})
}
-func (p *pageContentOutput) enableReuse() {
- p.reuseInit.Do(func() {
- p.reuse = true
- })
-}
-
// these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface {
targetPather
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index c281ad36c..04ca696c8 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -428,8 +428,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
assertFunc(t, e.ext, s.RegularPages())
- home, err := s.Info.Home()
- b.Assert(err, qt.IsNil)
+ home := s.Info.Home()
b.Assert(home, qt.Not(qt.IsNil))
b.Assert(home.File().Path(), qt.Equals, homePath)
b.Assert(content(home), qt.Contains, "Home Page Content")
@@ -1286,7 +1285,7 @@ func TestTranslationKey(t *testing.T) {
c.Assert(len(s.RegularPages()), qt.Equals, 2)
- home, _ := s.Info.Home()
+ home := s.Info.Home()
c.Assert(home, qt.Not(qt.IsNil))
c.Assert(home.TranslationKey(), qt.Equals, "home")
c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1")
diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go
index 1694b02ee..238c725bd 100644
--- a/hugolib/pagebundler_test.go
+++ b/hugolib/pagebundler_test.go
@@ -150,7 +150,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
c.Assert(leafBundle1.Section(), qt.Equals, "b")
sectionB := s.getPage(page.KindSection, "b")
c.Assert(sectionB, qt.Not(qt.IsNil))
- home, _ := s.Info.Home()
+ home := s.Info.Home()
c.Assert(home.BundleType(), qt.Equals, files.ContentClassBranch)
// This is a root bundle and should live in the "home section"
@@ -290,7 +290,7 @@ func TestPageBundlerSiteMultilingual(t *testing.T) {
c.Assert(len(s.RegularPages()), qt.Equals, 8)
c.Assert(len(s.Pages()), qt.Equals, 16)
- //dumpPages(s.AllPages()...)
+ // dumpPages(s.AllPages()...)
c.Assert(len(s.AllPages()), qt.Equals, 31)
diff --git a/hugolib/site.go b/hugolib/site.go
index 02380a6e7..57821ee93 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -30,6 +30,7 @@ import (
"strings"
"time"
+ "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/modules"
"golang.org/x/text/unicode/norm"
@@ -54,12 +55,11 @@ import (
"github.com/gohugoio/hugo/common/maps"
- "github.com/pkg/errors"
-
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/publisher"
+ "github.com/pkg/errors"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/langs"
@@ -1773,19 +1773,23 @@ var infoOnMissingLayout = map[string]bool{
"404": true,
}
-// hookRenderer is the canonical implementation of all hooks.ITEMRenderer,
+// hookRendererTemplate is the canonical implementation of all hooks.ITEMRenderer,
// where ITEM is the thing being hooked.
-type hookRenderer struct {
+type hookRendererTemplate struct {
templateHandler tpl.TemplateHandler
identity.SearchProvider
templ tpl.Template
}
-func (hr hookRenderer) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
+func (hr hookRendererTemplate) RenderLink(w io.Writer, ctx hooks.LinkContext) error {
+ return hr.templateHandler.Execute(hr.templ, w, ctx)
+}
+
+func (hr hookRendererTemplate) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
return hr.templateHandler.Execute(hr.templ, w, ctx)
}
-func (hr hookRenderer) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
+func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
return hr.templateHandler.Execute(hr.templ, w, ctx)
}
diff --git a/hugolib/site_sections.go b/hugolib/site_sections.go
index ae343716e..50dfe6ffa 100644
--- a/hugolib/site_sections.go
+++ b/hugolib/site_sections.go
@@ -19,14 +19,10 @@ import (
// Sections returns the top level sections.
func (s *SiteInfo) Sections() page.Pages {
- home, err := s.Home()
- if err == nil {
- return home.Sections()
- }
- return nil
+ return s.Home().Sections()
}
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
-func (s *SiteInfo) Home() (page.Page, error) {
- return s.s.home, nil
+func (s *SiteInfo) Home() page.Page {
+ return s.s.home
}
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index 180208a7b..30addfec6 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -21,6 +21,7 @@ import (
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/spf13/afero"
@@ -34,7 +35,7 @@ type ProviderConfig struct {
ContentFs afero.Fs
Logger loggers.Logger
Exec *hexec.Exec
- Highlight func(code, lang, optsStr string) (string, error)
+ highlight.Highlighter
}
// ProviderProvider creates converter providers.
@@ -127,9 +128,10 @@ type DocumentContext struct {
// RenderContext holds contextual information about the content to render.
type RenderContext struct {
- Src []byte
- RenderTOC bool
- RenderHooks hooks.Renderers
+ Src []byte
+ RenderTOC bool
+
+ GetRenderer hooks.GetRendererFunc
}
var FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")
diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go
index d36dad288..987cb1dc3 100644
--- a/markup/converter/hooks/hooks.go
+++ b/markup/converter/hooks/hooks.go
@@ -14,15 +14,17 @@
package hooks
import (
- "fmt"
"io"
- "strings"
+ "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
)
+var _ AttributesOptionsSliceProvider = (*attributes.AttributesHolder)(nil)
+
type AttributesProvider interface {
- Attributes() map[string]string
+ Attributes() map[string]interface{}
}
type LinkContext interface {
@@ -33,11 +35,30 @@ type LinkContext interface {
PlainText() string
}
+type CodeblockContext interface {
+ AttributesProvider
+ Options() map[string]interface{}
+ Lang() string
+ Code() string
+ Ordinal() int
+ Page() interface{}
+}
+
+type AttributesOptionsSliceProvider interface {
+ AttributesSlice() []attributes.Attribute
+ OptionsSlice() []attributes.Attribute
+}
+
type LinkRenderer interface {
RenderLink(w io.Writer, ctx LinkContext) error
identity.Provider
}
+type CodeBlockRenderer interface {
+ RenderCodeblock(w hugio.FlexiWriter, ctx CodeblockContext) error
+ identity.Provider
+}
+
// HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading.
type HeadingContext interface {
@@ -63,70 +84,13 @@ type HeadingRenderer interface {
identity.Provider
}
-type Renderers struct {
- LinkRenderer LinkRenderer
- ImageRenderer LinkRenderer
- HeadingRenderer HeadingRenderer
-}
-
-func (r Renderers) Eq(other interface{}) bool {
- ro, ok := other.(Renderers)
- if !ok {
- return false
- }
-
- if r.IsZero() || ro.IsZero() {
- return r.IsZero() && ro.IsZero()
- }
-
- var b1, b2 bool
- b1, b2 = r.ImageRenderer == nil, ro.ImageRenderer == nil
- if (b1 || b2) && (b1 != b2) {
- return false
- }
- if !b1 && r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() {
- return false
- }
-
- b1, b2 = r.LinkRenderer == nil, ro.LinkRenderer == nil
- if (b1 || b2) && (b1 != b2) {
- return false
- }
- if !b1 && r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() {
- return false
- }
-
- b1, b2 = r.HeadingRenderer == nil, ro.HeadingRenderer == nil
- if (b1 || b2) && (b1 != b2) {
- return false
- }
- if !b1 && r.HeadingRenderer.GetIdentity() != ro.HeadingRenderer.GetIdentity() {
- return false
- }
-
- return true
-}
-
-func (r Renderers) IsZero() bool {
- return r.HeadingRenderer == nil && r.LinkRenderer == nil && r.ImageRenderer == nil
-}
+type RendererType int
-func (r Renderers) String() string {
- if r.IsZero() {
- return "<zero>"
- }
-
- var sb strings.Builder
-
- if r.LinkRenderer != nil {
- sb.WriteString(fmt.Sprintf("LinkRenderer<%s>|", r.LinkRenderer.GetIdentity()))
- }
- if r.HeadingRenderer != nil {
- sb.WriteString(fmt.Sprintf("HeadingRenderer<%s>|", r.HeadingRenderer.GetIdentity()))
- }
- if r.ImageRenderer != nil {
- sb.WriteString(fmt.Sprintf("ImageRenderer<%s>|", r.ImageRenderer.GetIdentity()))
- }
+const (
+ LinkRendererType RendererType = iota + 1
+ ImageRendererType
+ HeadingRendererType
+ CodeBlockRendererType
+)
- return sb.String()
-}
+type GetRendererFunc func(t RendererType, id interface{}) interface{}
diff --git a/markup/goldmark/codeblocks/integration_test.go b/markup/goldmark/codeblocks/integration_test.go
new file mode 100644
index 000000000..d662b3911
--- /dev/null
+++ b/markup/goldmark/codeblocks/integration_test.go
@@ -0,0 +1,115 @@
+// 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 codeblocks_test
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestCodeblocks(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+[markup]
+ [markup.highlight]
+ anchorLineNos = false
+ codeFences = true
+ guessSyntax = false
+ hl_Lines = ''
+ lineAnchors = ''
+ lineNoStart = 1
+ lineNos = false
+ lineNumbersInTable = true
+ noClasses = false
+ style = 'monokai'
+ tabWidth = 4
+-- layouts/_default/_markup/render-codeblock-goat.html --
+{{ $diagram := diagrams.Goat .Code }}
+Goat SVG:{{ substr $diagram.SVG 0 100 | safeHTML }} }}|
+Goat Attribute: {{ .Attributes.width}}|
+-- layouts/_default/_markup/render-codeblock-go.html --
+Go Code: {{ .Code | safeHTML }}|
+Go Language: {{ .Lang }}|
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Ascii Diagram
+
+CODE_FENCEgoat { width="600" }
+--->
+CODE_FENCE
+
+## Go Code
+
+CODE_FENCEgo
+fmt.Println("Hello, World!");
+CODE_FENCE
+
+## Golang Code
+
+CODE_FENCEgolang
+fmt.Println("Hello, Golang!");
+CODE_FENCE
+
+## Bash Code
+
+CODE_FENCEbash { linenos=inline,hl_lines=[2,"5-6"],linenostart=32 class=blue }
+echo "l1";
+echo "l2";
+echo "l3";
+echo "l4";
+echo "l5";
+echo "l6";
+echo "l7";
+echo "l8";
+CODE_FENCE
+`
+
+ files = strings.ReplaceAll(files, "CODE_FENCE", "```")
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: false,
+ },
+ ).Build()
+
+ b.AssertFileContent("public/p1/index.html", `
+Goat SVG:<svg class='diagram'
+Goat Attribute: 600|
+
+Go Language: go|
+Go Code: fmt.Println("Hello, World!");
+
+Go Code: fmt.Println("Hello, Golang!");
+Go Language: golang|
+
+
+ `,
+ "Goat SVG:<svg class='diagram' xmlns='http://www.w3.org/2000/svg' version='1.1' height='25' width='40'",
+ "Goat Attribute: 600|",
+ "<h2 id=\"go-code\">Go Code</h2>\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|",
+ "<h2 id=\"golang-code\">Golang Code</h2>\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|",
+ "<h2 id=\"bash-code\">Bash Code</h2>\n<div class=\"highlight blue\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"ln\">32</span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;l1&#34;</span><span class=\"p\">;</span>\n</span></span><span class=\"line hl\"><span class=\"ln\">33</span>",
+ )
+}
diff --git a/markup/goldmark/codeblocks/render.go b/markup/goldmark/codeblocks/render.go
new file mode 100644
index 000000000..59d142e23
--- /dev/null
+++ b/markup/goldmark/codeblocks/render.go
@@ -0,0 +1,159 @@
+// 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 codeblocks
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+type (
+ diagrams struct{}
+ htmlRenderer struct{}
+)
+
+func New() goldmark.Extender {
+ return &diagrams{}
+}
+
+func (e *diagrams) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(
+ parser.WithASTTransformers(
+ util.Prioritized(&Transformer{}, 100),
+ ),
+ )
+ m.Renderer().AddOptions(renderer.WithNodeRenderers(
+ util.Prioritized(newHTMLRenderer(), 100),
+ ))
+}
+
+func newHTMLRenderer() renderer.NodeRenderer {
+ r := &htmlRenderer{}
+ return r
+}
+
+func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(KindCodeBlock, r.renderCodeBlock)
+}
+
+func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ ctx := w.(*render.Context)
+
+ if entering {
+ return ast.WalkContinue, nil
+ }
+
+ n := node.(*codeBlock)
+ lang := string(n.b.Language(src))
+ ordinal := n.ordinal
+
+ var buff bytes.Buffer
+
+ l := n.b.Lines().Len()
+ for i := 0; i < l; i++ {
+ line := n.b.Lines().At(i)
+ buff.Write(line.Value(src))
+ }
+ text := buff.String()
+
+ var info []byte
+ if n.b.Info != nil {
+ info = n.b.Info.Segment.Value(src)
+ }
+ attrs := getAttributes(n.b, info)
+
+ v := ctx.RenderContext().GetRenderer(hooks.CodeBlockRendererType, lang)
+ if v == nil {
+ return ast.WalkStop, fmt.Errorf("no code renderer found for %q", lang)
+ }
+
+ cr := v.(hooks.CodeBlockRenderer)
+
+ err := cr.RenderCodeblock(
+ w,
+ codeBlockContext{
+ page: ctx.DocumentContext().Document,
+ lang: lang,
+ code: text,
+ ordinal: ordinal,
+ AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerCodeBlock),
+ },
+ )
+
+ ctx.AddIdentity(cr)
+
+ return ast.WalkContinue, err
+}
+
+type codeBlockContext struct {
+ page interface{}
+ lang string
+ code string
+ ordinal int
+ *attributes.AttributesHolder
+}
+
+func (c codeBlockContext) Page() interface{} {
+ return c.page
+}
+
+func (c codeBlockContext) Lang() string {
+ return c.lang
+}
+
+func (c codeBlockContext) Code() string {
+ return c.code
+}
+
+func (c codeBlockContext) Ordinal() int {
+ return c.ordinal
+}
+
+func getAttributes(node *ast.FencedCodeBlock, infostr []byte) []ast.Attribute {
+ if node.Attributes() != nil {
+ return node.Attributes()
+ }
+ if infostr != nil {
+ attrStartIdx := -1
+
+ for idx, char := range infostr {
+ if char == '{' {
+ attrStartIdx = idx
+ break
+ }
+ }
+
+ if attrStartIdx > 0 {
+ n := ast.NewTextBlock() // dummy node for storing attributes
+ attrStr := infostr[attrStartIdx:]
+ if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr {
+ for _, attr := range attrs {
+ n.SetAttribute(attr.Name, attr.Value)
+ }
+ return n.Attributes()
+ }
+ }
+ }
+ return nil
+}
diff --git a/markup/goldmark/codeblocks/transform.go b/markup/goldmark/codeblocks/transform.go
new file mode 100644
index 000000000..791e99a5c
--- /dev/null
+++ b/markup/goldmark/codeblocks/transform.go
@@ -0,0 +1,53 @@
+package codeblocks
+
+import (
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+)
+
+// Kind is the kind of an Hugo code block.
+var KindCodeBlock = ast.NewNodeKind("HugoCodeBlock")
+
+// Its raw contents are the plain text of the code block.
+type codeBlock struct {
+ ast.BaseBlock
+ ordinal int
+ b *ast.FencedCodeBlock
+}
+
+func (*codeBlock) Kind() ast.NodeKind { return KindCodeBlock }
+
+func (*codeBlock) IsRaw() bool { return true }
+
+func (b *codeBlock) Dump(src []byte, level int) {
+}
+
+type Transformer struct{}
+
+// Transform transforms the provided Markdown AST.
+func (*Transformer) Transform(doc *ast.Document, reader text.Reader, pctx parser.Context) {
+ var codeBlocks []*ast.FencedCodeBlock
+
+ ast.Walk(doc, func(node ast.Node, enter bool) (ast.WalkStatus, error) {
+ if !enter {
+ return ast.WalkContinue, nil
+ }
+
+ cb, ok := node.(*ast.FencedCodeBlock)
+ if !ok {
+ return ast.WalkContinue, nil
+ }
+
+ codeBlocks = append(codeBlocks, cb)
+ return ast.WalkContinue, nil
+ })
+
+ for i, cb := range codeBlocks {
+ b := &codeBlock{b: cb, ordinal: i}
+ parent := cb.Parent()
+ if parent != nil {
+ parent.ReplaceChild(parent, cb, b)
+ }
+ }
+}
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
index c547fe1e0..4c1641a0b 100644
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -17,12 +17,12 @@ package goldmark
import (
"bytes"
"fmt"
- "math/bits"
"path/filepath"
"runtime/debug"
+ "github.com/gohugoio/hugo/markup/goldmark/codeblocks"
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
- "github.com/yuin/goldmark/ast"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/gohugoio/hugo/identity"
@@ -32,16 +32,13 @@ import (
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/markup/converter"
- "github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/yuin/goldmark"
- hl "github.com/yuin/goldmark-highlighting"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
- "github.com/yuin/goldmark/util"
)
// Provider is the package entry point.
@@ -104,7 +101,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
)
if mcfg.Highlight.CodeFences {
- extensions = append(extensions, newHighlighting(mcfg.Highlight))
+ extensions = append(extensions, codeblocks.New())
}
if cfg.Extensions.Table {
@@ -178,65 +175,6 @@ func (c converterResult) GetIdentities() identity.Identities {
return c.ids
}
-type bufWriter struct {
- *bytes.Buffer
-}
-
-const maxInt = 1<<(bits.UintSize-1) - 1
-
-func (b *bufWriter) Available() int {
- return maxInt
-}
-
-func (b *bufWriter) Buffered() int {
- return b.Len()
-}
-
-func (b *bufWriter) Flush() error {
- return nil
-}
-
-type renderContext struct {
- *bufWriter
- positions []int
- renderContextData
-}
-
-func (ctx *renderContext) pushPos(n int) {
- ctx.positions = append(ctx.positions, n)
-}
-
-func (ctx *renderContext) popPos() int {
- i := len(ctx.positions) - 1
- p := ctx.positions[i]
- ctx.positions = ctx.positions[:i]
- return p
-}
-
-type renderContextData interface {
- RenderContext() converter.RenderContext
- DocumentContext() converter.DocumentContext
- AddIdentity(id identity.Provider)
-}
-
-type renderContextDataHolder struct {
- rctx converter.RenderContext
- dctx converter.DocumentContext
- ids identity.Manager
-}
-
-func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext {
- return ctx.rctx
-}
-
-func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext {
- return ctx.dctx
-}
-
-func (ctx *renderContextDataHolder) AddIdentity(id identity.Provider) {
- ctx.ids.Add(id)
-}
-
var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
@@ -251,7 +189,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
}
}()
- buf := &bufWriter{Buffer: &bytes.Buffer{}}
+ buf := &render.BufWriter{Buffer: &bytes.Buffer{}}
result = buf
pctx := c.newParserContext(ctx)
reader := text.NewReader(ctx.Src)
@@ -261,15 +199,15 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
parser.WithContext(pctx),
)
- rcx := &renderContextDataHolder{
- rctx: ctx,
- dctx: c.ctx,
- ids: identity.NewManager(converterIdentity),
+ rcx := &render.RenderContextDataHolder{
+ Rctx: ctx,
+ Dctx: c.ctx,
+ IDs: identity.NewManager(converterIdentity),
}
- w := &renderContext{
- bufWriter: buf,
- renderContextData: rcx,
+ w := &render.Context{
+ BufWriter: buf,
+ ContextData: rcx,
}
if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil {
@@ -278,7 +216,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
return converterResult{
Result: buf,
- ids: rcx.ids.GetIdentities(),
+ ids: rcx.IDs.GetIdentities(),
toc: pctx.TableOfContents(),
}, nil
}
@@ -309,63 +247,3 @@ func (p *parserContext) TableOfContents() tableofcontents.Root {
}
return tableofcontents.Root{}
}
-
-func newHighlighting(cfg highlight.Config) goldmark.Extender {
- return hl.NewHighlighting(
- hl.WithStyle(cfg.Style),
- hl.WithGuessLanguage(cfg.GuessSyntax),
- hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
- hl.WithFormatOptions(
- cfg.ToHTMLOptions()...,
- ),
-
- hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) {
- var language string
- if l, hasLang := ctx.Language(); hasLang {
- language = string(l)
- }
-
- if ctx.Highlighted() {
- if entering {
- writeDivStart(w, ctx)
- } else {
- writeDivEnd(w)
- }
- } else {
- if entering {
- highlight.WritePreStart(w, language, "")
- } else {
- highlight.WritePreEnd(w)
- }
- }
- }),
- )
-}
-
-func writeDivStart(w util.BufWriter, ctx hl.CodeBlockContext) {
- w.WriteString(`<div class="highlight`)
-
- var attributes []ast.Attribute
- if ctx.Attributes() != nil {
- attributes = ctx.Attributes().All()
- }
-
- if attributes != nil {
- class, found := ctx.Attributes().GetString("class")
- if found {
- w.WriteString(" ")
- w.Write(util.EscapeHTML(class.([]byte)))
-
- }
- _, _ = w.WriteString("\"")
- renderAttributes(w, true, attributes...)
- } else {
- _, _ = w.WriteString("\"")
- }
-
- w.WriteString(">")
-}
-
-func writeDivEnd(w util.BufWriter) {
- w.WriteString("</div>")
-}
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
index 684f22c54..ecb308eba 100644
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -20,6 +20,7 @@ import (
"github.com/spf13/cast"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/highlight"
@@ -41,9 +42,18 @@ func convert(c *qt.C, mconf markup_config.Config, content string) converter.Resu
},
)
c.Assert(err, qt.IsNil)
+ h := highlight.New(mconf.Highlight)
+
+ getRenderer := func(t hooks.RendererType, id interface{}) interface{} {
+ if t == hooks.CodeBlockRendererType {
+ return h
+ }
+ return nil
+ }
+
conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
c.Assert(err, qt.IsNil)
- b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)})
+ b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content), GetRenderer: getRenderer})
c.Assert(err, qt.IsNil)
return b
@@ -372,12 +382,21 @@ LINE5
},
)
+ h := highlight.New(conf)
+
+ getRenderer := func(t hooks.RendererType, id interface{}) interface{} {
+ if t == hooks.CodeBlockRendererType {
+ return h
+ }
+ return nil
+ }
+
content := "```" + language + "\n" + code + "\n```"
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
- b, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
+ b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer})
c.Assert(err, qt.IsNil)
return string(b.Bytes())
@@ -391,7 +410,7 @@ LINE5
// TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func.
c.Assert(result, qt.Equals, "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">&#34;Hugo Rocks!&#34;</span>\n</span></span></code></pre></div>")
result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown")
- c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo &quot;Hugo Rocks!&quot;\n</code></pre>")
+ c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo &#34;Hugo Rocks!&#34;\n</code></pre>")
})
c.Run("Highlight lines, default config", func(c *qt.C) {
diff --git a/markup/goldmark/integration_test.go b/markup/goldmark/integration_test.go
index 4ace04f75..f1fa745c5 100644
--- a/markup/goldmark/integration_test.go
+++ b/markup/goldmark/integration_test.go
@@ -36,12 +36,12 @@ func TestAttributeExclusion(t *testing.T) {
---
title: "p1"
---
-## Heading {class="a" onclick="alert('heading')" linenos="inline"}
+## Heading {class="a" onclick="alert('heading')"}
> Blockquote
-{class="b" ondblclick="alert('blockquote')" LINENOS="inline"}
+{class="b" ondblclick="alert('blockquote')"}
-~~~bash {id="c" onmouseover="alert('code fence')"}
+~~~bash {id="c" onmouseover="alert('code fence')" LINENOS=true}
foo
~~~
-- layouts/_default/single.html --
@@ -96,6 +96,63 @@ title: "p1"
`)
}
+func TestAttributesDefaultRenderer(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- content/p1.md --
+---
+title: "p1"
+---
+## Heading Attribute Which Needs Escaping { class="a < b" }
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: false,
+ },
+ ).Build()
+
+ b.AssertFileContent("public/p1/index.html", `
+class="a &lt; b"
+ `)
+}
+
+// Issue 9558.
+func TestAttributesHookNoEscape(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- content/p1.md --
+---
+title: "p1"
+---
+## Heading Attribute Which Needs Escaping { class="Smith & Wesson" }
+-- layouts/_default/_markup/render-heading.html --
+plain: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v }}|{{ end }}|
+safeHTML: |{{- range $k, $v := .Attributes -}}{{ $k }}: {{ $v | safeHTML }}|{{ end }}|
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: false,
+ },
+ ).Build()
+
+ b.AssertFileContent("public/p1/index.html", `
+plain: |class: Smith &amp; Wesson|id: heading-attribute-which-needs-escaping|
+safeHTML: |class: Smith & Wesson|id: heading-attribute-which-needs-escaping|
+ `)
+}
+
// Issue 9504
func TestLinkInTitle(t *testing.T) {
t.Parallel()
@@ -132,6 +189,84 @@ title: "p1"
)
}
+func TestHighlight(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- config.toml --
+[markup]
+[markup.highlight]
+anchorLineNos = false
+codeFences = true
+guessSyntax = false
+hl_Lines = ''
+lineAnchors = ''
+lineNoStart = 1
+lineNos = false
+lineNumbersInTable = true
+noClasses = false
+style = 'monokai'
+tabWidth = 4
+-- layouts/_default/single.html --
+{{ .Content }}
+-- content/p1.md --
+---
+title: "p1"
+---
+
+## Code Fences
+
+§§§bash
+LINE1
+§§§
+
+## Code Fences No Lexer
+
+§§§moo
+LINE1
+§§§
+
+## Code Fences Simple Attributes
+
+§§A§bash { .myclass id="myid" }
+LINE1
+§§A§
+
+## Code Fences Line Numbers
+
+§§§bash {linenos=table,hl_lines=[8,"15-17"],linenostart=199}
+LINE1
+LINE2
+LINE3
+LINE4
+LINE5
+LINE6
+LINE7
+LINE8
+§§§
+
+
+
+
+`
+
+ // Code fences
+ files = strings.ReplaceAll(files, "§§§", "```")
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ },
+ ).Build()
+
+ b.AssertFileContent("public/p1/index.html",
+ "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\">LINE1\n</span></span></code></pre></div>",
+ "Code Fences No Lexer</h2>\n<pre tabindex=\"0\"><code class=\"language-moo\" data-lang=\"moo\">LINE1\n</code></pre>",
+ "lnt",
+ )
+}
+
func BenchmarkRenderHooks(b *testing.B) {
files := `
-- config.toml --
diff --git a/markup/goldmark/internal/render/context.go b/markup/goldmark/internal/render/context.go
new file mode 100644
index 000000000..b18983ef3
--- /dev/null
+++ b/markup/goldmark/internal/render/context.go
@@ -0,0 +1,81 @@
+// 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 render
+
+import (
+ "bytes"
+ "math/bits"
+
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/markup/converter"
+)
+
+type BufWriter struct {
+ *bytes.Buffer
+}
+
+const maxInt = 1<<(bits.UintSize-1) - 1
+
+func (b *BufWriter) Available() int {
+ return maxInt
+}
+
+func (b *BufWriter) Buffered() int {
+ return b.Len()
+}
+
+func (b *BufWriter) Flush() error {
+ return nil
+}
+
+type Context struct {
+ *BufWriter
+ positions []int
+ ContextData
+}
+
+func (ctx *Context) PushPos(n int) {
+ ctx.positions = append(ctx.positions, n)
+}
+
+func (ctx *Context) PopPos() int {
+ i := len(ctx.positions) - 1
+ p := ctx.positions[i]
+ ctx.positions = ctx.positions[:i]
+ return p
+}
+
+type ContextData interface {
+ RenderContext() converter.RenderContext
+ DocumentContext() converter.DocumentContext
+ AddIdentity(id identity.Provider)
+}
+
+type RenderContextDataHolder struct {
+ Rctx converter.RenderContext
+ Dctx converter.DocumentContext
+ IDs identity.Manager
+}
+
+func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
+ return ctx.Rctx
+}
+
+func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
+ return ctx.Dctx
+}
+
+func (ctx *RenderContextDataHolder) AddIdentity(id identity.Provider) {
+ ctx.IDs.Add(id)
+}
diff --git a/markup/goldmark/render_hooks.go b/markup/goldmark/render_hooks.go
index 1862c2125..d5e35380a 100644
--- a/markup/goldmark/render_hooks.go
+++ b/markup/goldmark/render_hooks.go
@@ -16,11 +16,10 @@ package goldmark
import (
"bytes"
"strings"
- "sync"
-
- "github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/goldmark/internal/render"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
@@ -44,28 +43,6 @@ func newLinks() goldmark.Extender {
return &links{}
}
-type attributesHolder struct {
- // What we get from Goldmark.
- astAttributes []ast.Attribute
-
- // What we send to the the render hooks.
- attributesInit sync.Once
- attributes map[string]string
-}
-
-func (a *attributesHolder) Attributes() map[string]string {
- a.attributesInit.Do(func() {
- a.attributes = make(map[string]string)
- for _, attr := range a.astAttributes {
- if strings.HasPrefix(string(attr.Name), "on") {
- continue
- }
- a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
- }
- })
- return a.attributes
-}
-
type linkContext struct {
page interface{}
destination string
@@ -104,7 +81,7 @@ type headingContext struct {
anchor string
text string
plainText string
- *attributesHolder
+ *attributes.AttributesHolder
}
func (ctx headingContext) Page() interface{} {
@@ -143,52 +120,17 @@ func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
reg.Register(ast.KindHeading, r.renderHeading)
}
-func (r *hookedRenderer) renderAttributesForNode(w util.BufWriter, node ast.Node) {
- renderAttributes(w, false, node.Attributes()...)
-}
-
-// Attributes with special meaning that does not make sense to render in HTML.
-var attributeExcludes = map[string]bool{
- "hl_lines": true,
- "hl_style": true,
- "linenos": true,
- "linenostart": true,
-}
-
-func renderAttributes(w util.BufWriter, skipClass bool, attributes ...ast.Attribute) {
- for _, attr := range attributes {
- if skipClass && bytes.Equal(attr.Name, []byte("class")) {
- continue
- }
-
- a := strings.ToLower(string(attr.Name))
- if attributeExcludes[a] || strings.HasPrefix(a, "on") {
- continue
- }
-
- _, _ = w.WriteString(" ")
- _, _ = w.Write(attr.Name)
- _, _ = w.WriteString(`="`)
-
- switch v := attr.Value.(type) {
- case []byte:
- _, _ = w.Write(util.EscapeHTML(v))
- default:
- w.WriteString(cast.ToString(v))
- }
-
- _ = w.WriteByte('"')
- }
-}
-
func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Image)
- var h hooks.Renderers
+ var lr hooks.LinkRenderer
- ctx, ok := w.(*renderContext)
+ ctx, ok := w.(*render.Context)
if ok {
- h = ctx.RenderContext().RenderHooks
- ok = h.ImageRenderer != nil
+ h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
+ ok = h != nil
+ if ok {
+ lr = h.(hooks.LinkRenderer)
+ }
}
if !ok {
@@ -197,15 +139,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
if entering {
// Store the current pos so we can capture the rendered text.
- ctx.pushPos(ctx.Buffer.Len())
+ ctx.PushPos(ctx.Buffer.Len())
return ast.WalkContinue, nil
}
- pos := ctx.popPos()
+ pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos)
- err := h.ImageRenderer.RenderLink(
+ err := lr.RenderLink(
w,
linkContext{
page: ctx.DocumentContext().Document,
@@ -216,7 +158,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
},
)
- ctx.AddIdentity(h.ImageRenderer)
+ ctx.AddIdentity(lr)
return ast.WalkContinue, err
}
@@ -250,12 +192,15 @@ func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, nod
func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
- var h hooks.Renderers
+ var lr hooks.LinkRenderer
- ctx, ok := w.(*renderContext)
+ ctx, ok := w.(*render.Context)
if ok {
- h = ctx.RenderContext().RenderHooks
- ok = h.LinkRenderer != nil
+ h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
+ ok = h != nil
+ if ok {
+ lr = h.(hooks.LinkRenderer)
+ }
}
if !ok {
@@ -264,15 +209,15 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
if entering {
// Store the current pos so we can capture the rendered text.
- ctx.pushPos(ctx.Buffer.Len())
+ ctx.PushPos(ctx.Buffer.Len())
return ast.WalkContinue, nil
}
- pos := ctx.popPos()
+ pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos)
- err := h.LinkRenderer.RenderLink(
+ err := lr.RenderLink(
w,
linkContext{
page: ctx.DocumentContext().Document,
@@ -286,7 +231,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
// but for now it's important that it's not .GetIdentity() that's added here,
// to make sure we search the entire chain on changes.
- ctx.AddIdentity(h.LinkRenderer)
+ ctx.AddIdentity(lr)
return ast.WalkContinue, err
}
@@ -319,12 +264,15 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
}
n := node.(*ast.AutoLink)
- var h hooks.Renderers
+ var lr hooks.LinkRenderer
- ctx, ok := w.(*renderContext)
+ ctx, ok := w.(*render.Context)
if ok {
- h = ctx.RenderContext().RenderHooks
- ok = h.LinkRenderer != nil
+ h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
+ ok = h != nil
+ if ok {
+ lr = h.(hooks.LinkRenderer)
+ }
}
if !ok {
@@ -337,7 +285,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
url = "mailto:" + url
}
- err := h.LinkRenderer.RenderLink(
+ err := lr.RenderLink(
w,
linkContext{
page: ctx.DocumentContext().Document,
@@ -350,7 +298,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
// TODO(bep) I have a working branch that fixes these rather confusing identity types,
// but for now it's important that it's not .GetIdentity() that's added here,
// to make sure we search the entire chain on changes.
- ctx.AddIdentity(h.LinkRenderer)
+ ctx.AddIdentity(lr)
return ast.WalkContinue, err
}
@@ -383,12 +331,15 @@ func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte,
func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
- var h hooks.Renderers
+ var hr hooks.HeadingRenderer
- ctx, ok := w.(*renderContext)
+ ctx, ok := w.(*render.Context)
if ok {
- h = ctx.RenderContext().RenderHooks
- ok = h.HeadingRenderer != nil
+ h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
+ ok = h != nil
+ if ok {
+ hr = h.(hooks.HeadingRenderer)
+ }
}
if !ok {
@@ -397,11 +348,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
if entering {
// Store the current pos so we can capture the rendered text.
- ctx.pushPos(ctx.Buffer.Len())
+ ctx.PushPos(ctx.Buffer.Len())
return ast.WalkContinue, nil
}
- pos := ctx.popPos()
+ pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos)
// All ast.Heading nodes are guaranteed to have an attribute called "id"
@@ -409,7 +360,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
anchori, _ := n.AttributeString("id")
anchor := anchori.([]byte)
- err := h.HeadingRenderer.RenderHeading(
+ err := hr.RenderHeading(
w,
headingContext{
page: ctx.DocumentContext().Document,
@@ -417,11 +368,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
anchor: string(anchor),
text: string(text),
plainText: string(n.Text(source)),
- attributesHolder: &attributesHolder{astAttributes: n.Attributes()},
+ AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
},
)
- ctx.AddIdentity(h.HeadingRenderer)
+ ctx.AddIdentity(hr)
return ast.WalkContinue, err
}
@@ -432,7 +383,7 @@ func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, n
_, _ = w.WriteString("<h")
_ = w.WriteByte("0123456"[n.Level])
if n.Attributes() != nil {
- r.renderAttributesForNode(w, node)
+ attributes.RenderASTAttributes(w, node.Attributes()...)
}
_ = w.WriteByte('>')
} else {
diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/toc_test.go
index f8fcf79d4..6e080bf46 100644
--- a/markup/goldmark/toc_test.go
+++ b/markup/goldmark/toc_test.go
@@ -18,6 +18,7 @@ import (
"strings"
"testing"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/common/loggers"
@@ -27,6 +28,8 @@ import (
qt "github.com/frankban/quicktest"
)
+var nopGetRenderer = func(t hooks.RendererType, id interface{}) interface{} { return nil }
+
func TestToc(t *testing.T) {
c := qt.New(t)
@@ -58,7 +61,7 @@ And then some.
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
- b, err := conv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+ b, err := conv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
c.Assert(err, qt.IsNil)
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -108,7 +111,7 @@ func TestEscapeToc(t *testing.T) {
"# `echo codeblock`",
}, "\n")
// content := ""
- b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+ b, err := safeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
c.Assert(err, qt.IsNil)
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -120,7 +123,7 @@ func TestEscapeToc(t *testing.T) {
</ul>
</nav>`, qt.Commentf(got))
- b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true})
+ b, err = unsafeConv.Convert(converter.RenderContext{Src: []byte(content), RenderTOC: true, GetRenderer: nopGetRenderer})
c.Assert(err, qt.IsNil)
got = b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents">
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
index 1dc1e28e2..86ac02c3d 100644
--- a/markup/highlight/config.go
+++ b/markup/highlight/config.go
@@ -20,6 +20,7 @@ import (
"strings"
"github.com/alecthomas/chroma/formatters/html"
+ "github.com/spf13/cast"
"github.com/gohugoio/hugo/config"
@@ -46,6 +47,9 @@ type Config struct {
// Use inline CSS styles.
NoClasses bool
+ // No highlighting.
+ NoHl bool
+
// When set, line numbers will be printed.
LineNos bool
LineNumbersInTable bool
@@ -60,6 +64,9 @@ type Config struct {
// A space separated list of line numbers, e.g. “3-8 10-20”.
Hl_Lines string
+ // A parsed and ready to use list of line ranges.
+ HL_lines_parsed [][2]int
+
// TabWidth sets the number of characters for a tab. Defaults to 4.
TabWidth int
@@ -80,9 +87,19 @@ func (cfg Config) ToHTMLOptions() []html.Option {
html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
}
- if cfg.Hl_Lines != "" {
- ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
- if err == nil {
+ if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
+ var ranges [][2]int
+ if cfg.HL_lines_parsed != nil {
+ ranges = cfg.HL_lines_parsed
+ } else {
+ var err error
+ ranges, err = hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
+ if err != nil {
+ ranges = nil
+ }
+ }
+
+ if ranges != nil {
options = append(options, html.HighlightLines(ranges))
}
}
@@ -90,14 +107,32 @@ func (cfg Config) ToHTMLOptions() []html.Option {
return options
}
+func applyOptions(opts interface{}, cfg *Config) error {
+ if opts == nil {
+ return nil
+ }
+ switch vv := opts.(type) {
+ case map[string]interface{}:
+ return applyOptionsFromMap(vv, cfg)
+ case string:
+ return applyOptionsFromString(vv, cfg)
+ }
+ return nil
+}
+
func applyOptionsFromString(opts string, cfg *Config) error {
- optsm, err := parseOptions(opts)
+ optsm, err := parseHightlightOptions(opts)
if err != nil {
return err
}
return mapstructure.WeakDecode(optsm, cfg)
}
+func applyOptionsFromMap(optsm map[string]interface{}, cfg *Config) error {
+ normalizeHighlightOptions(optsm)
+ return mapstructure.WeakDecode(optsm, cfg)
+}
+
// ApplyLegacyConfig applies legacy config from back when we had
// Pygments.
func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
@@ -128,7 +163,7 @@ func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
return nil
}
-func parseOptions(in string) (map[string]interface{}, error) {
+func parseHightlightOptions(in string) (map[string]interface{}, error) {
in = strings.Trim(in, " ")
opts := make(map[string]interface{})
@@ -142,19 +177,57 @@ func parseOptions(in string) (map[string]interface{}, error) {
if len(keyVal) != 2 {
return opts, fmt.Errorf("invalid Highlight option: %s", key)
}
- if key == "linenos" {
- opts[key] = keyVal[1] != "false"
- if keyVal[1] == "table" || keyVal[1] == "inline" {
- opts["lineNumbersInTable"] = keyVal[1] == "table"
- }
- } else {
- opts[key] = keyVal[1]
- }
+ opts[key] = keyVal[1]
+
}
+ normalizeHighlightOptions(opts)
+
return opts, nil
}
+func normalizeHighlightOptions(m map[string]interface{}) {
+ if m == nil {
+ return
+ }
+
+ const (
+ lineNosKey = "linenos"
+ hlLinesKey = "hl_lines"
+ linosStartKey = "linenostart"
+ noHlKey = "nohl"
+ )
+
+ baseLineNumber := 1
+ if v, ok := m[linosStartKey]; ok {
+ baseLineNumber = cast.ToInt(v)
+ }
+
+ for k, v := range m {
+ switch k {
+ case noHlKey:
+ m[noHlKey] = cast.ToBool(v)
+ case lineNosKey:
+ if v == "table" || v == "inline" {
+ m["lineNumbersInTable"] = v == "table"
+ }
+ if vs, ok := v.(string); ok {
+ m[k] = vs != "false"
+ }
+
+ case hlLinesKey:
+ if hlRanges, ok := v.([][2]int); ok {
+ for i := range hlRanges {
+ hlRanges[i][0] += baseLineNumber
+ hlRanges[i][1] += baseLineNumber
+ }
+ delete(m, k)
+ m[k+"_parsed"] = hlRanges
+ }
+ }
+ }
+}
+
// startLine compensates for https://github.com/alecthomas/chroma/issues/30
func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
var ranges [][2]int
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
index 319426241..e9cbeb3c9 100644
--- a/markup/highlight/highlight.go
+++ b/markup/highlight/highlight.go
@@ -16,47 +16,155 @@ package highlight
import (
"fmt"
gohtml "html"
+ "html/template"
"io"
+ "strconv"
"strings"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
- hl "github.com/yuin/goldmark-highlighting"
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/gohugoio/hugo/identity"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/internal/attributes"
)
+// Markdown attributes used by the Chroma hightlighter.
+var chromaHightlightProcessingAttributes = map[string]bool{
+ "anchorLineNos": true,
+ "guessSyntax": true,
+ "hl_Lines": true,
+ "lineAnchors": true,
+ "lineNos": true,
+ "lineNoStart": true,
+ "lineNumbersInTable": true,
+ "noClasses": true,
+ "style": true,
+ "tabWidth": true,
+}
+
+func init() {
+ for k, v := range chromaHightlightProcessingAttributes {
+ chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
+ }
+}
+
func New(cfg Config) Highlighter {
- return Highlighter{
+ return chromaHighlighter{
cfg: cfg,
}
}
-type Highlighter struct {
+type Highlighter interface {
+ Highlight(code, lang string, opts interface{}) (string, error)
+ HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error)
+ hooks.CodeBlockRenderer
+}
+
+type chromaHighlighter struct {
cfg Config
}
-func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
- if optsStr == "" {
- return highlight(code, lang, h.cfg)
+func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) {
+ cfg := h.cfg
+ if err := applyOptions(opts, &cfg); err != nil {
+ return "", err
}
+ var b strings.Builder
- cfg := h.cfg
- if err := applyOptionsFromString(optsStr, &cfg); err != nil {
+ if err := highlight(&b, code, lang, nil, cfg); err != nil {
return "", err
}
- return highlight(code, lang, cfg)
+ return b.String(), nil
}
-func highlight(code, lang string, cfg Config) (string, error) {
- w := &strings.Builder{}
+func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) {
+ cfg := h.cfg
+
+ var b strings.Builder
+
+ attributes := ctx.(hooks.AttributesOptionsSliceProvider).AttributesSlice()
+ options := ctx.Options()
+
+ if err := applyOptionsFromMap(options, &cfg); err != nil {
+ return HightlightResult{}, err
+ }
+
+ // Apply these last so the user can override them.
+ if err := applyOptions(opts, &cfg); err != nil {
+ return HightlightResult{}, err
+ }
+
+ err := highlight(&b, ctx.Code(), ctx.Lang(), attributes, cfg)
+ if err != nil {
+ return HightlightResult{}, err
+ }
+
+ return HightlightResult{
+ Body: template.HTML(b.String()),
+ }, nil
+}
+
+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 {
+ return err
+ }
+
+ return highlight(w, ctx.Code(), ctx.Lang(), attributes, cfg)
+}
+
+var id = identity.NewPathIdentity("chroma", "highlight")
+
+func (h chromaHighlighter) GetIdentity() identity.Identity {
+ return id
+}
+
+type HightlightResult struct {
+ Body template.HTML
+}
+
+func (h HightlightResult) Highlighted() template.HTML {
+ return h.Body
+}
+
+func (h chromaHighlighter) toHighlightOptionsAttributes(ctx hooks.CodeblockContext) (map[string]interface{}, map[string]interface{}) {
+ attributes := ctx.Attributes()
+ if attributes == nil || len(attributes) == 0 {
+ return nil, nil
+ }
+
+ options := make(map[string]interface{})
+ attrs := make(map[string]interface{})
+
+ for k, v := range attributes {
+ klow := strings.ToLower(k)
+ if chromaHightlightProcessingAttributes[klow] {
+ options[klow] = v
+ } else {
+ attrs[k] = v
+ }
+ }
+ const lineanchorsKey = "lineanchors"
+ if _, found := options[lineanchorsKey]; !found {
+ // Set it to the ordinal.
+ options[lineanchorsKey] = strconv.Itoa(ctx.Ordinal())
+ }
+ return options, attrs
+}
+
+func highlight(w hugio.FlexiWriter, code, lang string, attributes []attributes.Attribute, cfg Config) error {
var lexer chroma.Lexer
if lang != "" {
lexer = lexers.Get(lang)
}
- if lexer == nil && cfg.GuessSyntax {
+ if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
lexer = lexers.Analyse(code)
if lexer == nil {
lexer = lexers.Fallback
@@ -69,7 +177,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
fmt.Fprint(w, wrapper.Start(true, ""))
fmt.Fprint(w, gohtml.EscapeString(code))
fmt.Fprint(w, wrapper.End(true))
- return w.String(), nil
+ return nil
}
style := styles.Get(cfg.Style)
@@ -80,7 +188,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
iterator, err := lexer.Tokenise(nil, code)
if err != nil {
- return "", err
+ return err
}
options := cfg.ToHTMLOptions()
@@ -88,25 +196,13 @@ func highlight(code, lang string, cfg Config) (string, error) {
formatter := html.New(options...)
- fmt.Fprint(w, `<div class="highlight">`)
+ writeDivStart(w, attributes)
if err := formatter.Format(w, style, iterator); err != nil {
- return "", err
+ return err
}
- fmt.Fprint(w, `</div>`)
-
- return w.String(), nil
-}
+ writeDivEnd(w)
-func GetCodeBlockOptions() func(ctx hl.CodeBlockContext) []html.Option {
- return func(ctx hl.CodeBlockContext) []html.Option {
- var language string
- if l, ok := ctx.Language(); ok {
- language = string(l)
- }
- return []html.Option{
- getHtmlPreWrapper(language),
- }
- }
+ return nil
}
func getPreWrapper(language string) preWrapper {
@@ -150,3 +246,25 @@ func (p preWrapper) End(code bool) string {
func WritePreEnd(w io.Writer) {
fmt.Fprint(w, preEnd)
}
+
+func writeDivStart(w hugio.FlexiWriter, attrs []attributes.Attribute) {
+ w.WriteString(`<div class="highlight`)
+ if attrs != nil {
+ for _, attr := range attrs {
+ if attr.Name == "class" {
+ w.WriteString(" " + attr.ValueString())
+ break
+ }
+ }
+ _, _ = w.WriteString("\"")
+ attributes.RenderAttributes(w, true, attrs...)
+ } else {
+ _, _ = w.WriteString("\"")
+ }
+
+ w.WriteString(">")
+}
+
+func writeDivEnd(w hugio.FlexiWriter) {
+ w.WriteString("</div>")
+}
diff --git a/markup/internal/attributes/attributes.go b/markup/internal/attributes/attributes.go
new file mode 100644
index 000000000..1cce7edd1
--- /dev/null
+++ b/markup/internal/attributes/attributes.go
@@ -0,0 +1,219 @@
+// 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 attributes
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+
+ "github.com/gohugoio/hugo/common/hugio"
+ "github.com/spf13/cast"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/util"
+)
+
+// Markdown attributes used as options by the Chroma highlighter.
+var chromaHightlightProcessingAttributes = map[string]bool{
+ "anchorLineNos": true,
+ "guessSyntax": true,
+ "hl_Lines": true,
+ "lineAnchors": true,
+ "lineNos": true,
+ "lineNoStart": true,
+ "lineNumbersInTable": true,
+ "noClasses": true,
+ "nohl": true,
+ "style": true,
+ "tabWidth": true,
+}
+
+func init() {
+ for k, v := range chromaHightlightProcessingAttributes {
+ chromaHightlightProcessingAttributes[strings.ToLower(k)] = v
+ }
+}
+
+type AttributesOwnerType int
+
+const (
+ AttributesOwnerGeneral AttributesOwnerType = iota
+ AttributesOwnerCodeBlock
+)
+
+func New(astAttributes []ast.Attribute, ownerType AttributesOwnerType) *AttributesHolder {
+ var (
+ attrs []Attribute
+ opts []Attribute
+ )
+ for _, v := range astAttributes {
+ nameLower := strings.ToLower(string(v.Name))
+ if strings.HasPrefix(string(nameLower), "on") {
+ continue
+ }
+ var vv interface{}
+ switch vvv := v.Value.(type) {
+ case bool, float64:
+ vv = vvv
+ case []interface{}:
+ // Highlight line number hlRanges.
+ var hlRanges [][2]int
+ for _, l := range vvv {
+ if ln, ok := l.(float64); ok {
+ hlRanges = append(hlRanges, [2]int{int(ln) - 1, int(ln) - 1})
+ } else if rng, ok := l.([]uint8); ok {
+ slices := strings.Split(string([]byte(rng)), "-")
+ lhs, err := strconv.Atoi(slices[0])
+ if err != nil {
+ continue
+ }
+ rhs := lhs
+ if len(slices) > 1 {
+ rhs, err = strconv.Atoi(slices[1])
+ if err != nil {
+ continue
+ }
+ }
+ hlRanges = append(hlRanges, [2]int{lhs - 1, rhs - 1})
+ }
+ }
+ vv = hlRanges
+ case []byte:
+ // Note that we don't do any HTML escaping here.
+ // We used to do that, but that changed in #9558.
+ // Noww it's up to the templates to decide.
+ vv = string(vvv)
+ default:
+ panic(fmt.Sprintf("not implemented: %T", vvv))
+ }
+
+ if ownerType == AttributesOwnerCodeBlock && chromaHightlightProcessingAttributes[nameLower] {
+ attr := Attribute{Name: string(v.Name), Value: vv}
+ opts = append(opts, attr)
+ } else {
+ attr := Attribute{Name: nameLower, Value: vv}
+ attrs = append(attrs, attr)
+ }
+
+ }
+
+ return &AttributesHolder{
+ attributes: attrs,
+ options: opts,
+ }
+}
+
+type Attribute struct {
+ Name string
+ Value interface{}
+}
+
+func (a Attribute) ValueString() string {
+ return cast.ToString(a.Value)
+}
+
+type AttributesHolder struct {
+ // What we get from Goldmark.
+ attributes []Attribute
+
+ // Attributes considered to be an option (code blocks)
+ options []Attribute
+
+ // What we send to the the render hooks.
+ attributesMapInit sync.Once
+ attributesMap map[string]interface{}
+ optionsMapInit sync.Once
+ optionsMap map[string]interface{}
+}
+
+type Attributes map[string]interface{}
+
+func (a *AttributesHolder) Attributes() map[string]interface{} {
+ a.attributesMapInit.Do(func() {
+ a.attributesMap = make(map[string]interface{})
+ for _, v := range a.attributes {
+ a.attributesMap[v.Name] = v.Value
+ }
+ })
+ return a.attributesMap
+}
+
+func (a *AttributesHolder) Options() map[string]interface{} {
+ a.optionsMapInit.Do(func() {
+ a.optionsMap = make(map[string]interface{})
+ for _, v := range a.options {
+ a.optionsMap[v.Name] = v.Value
+ }
+ })
+ return a.optionsMap
+}
+
+func (a *AttributesHolder) AttributesSlice() []Attribute {
+ return a.attributes
+}
+
+func (a *AttributesHolder) OptionsSlice() []Attribute {
+ return a.options
+}
+
+// RenderASTAttributes writes the AST attributes to the given as attributes to an HTML element.
+// This is used by the default HTML renderers, e.g. for headings etc. where no hook template could be found.
+// This performs HTML esacaping of string attributes.
+func RenderASTAttributes(w hugio.FlexiWriter, attributes ...ast.Attribute) {
+ for _, attr := range attributes {
+
+ a := strings.ToLower(string(attr.Name))
+ if strings.HasPrefix(a, "on") {
+ continue
+ }
+
+ _, _ = w.WriteString(" ")
+ _, _ = w.Write(attr.Name)
+ _, _ = w.WriteString(`="`)
+
+ switch v := attr.Value.(type) {
+ case []byte:
+ _, _ = w.Write(util.EscapeHTML(v))
+ default:
+ w.WriteString(cast.ToString(v))
+ }
+
+ _ = w.WriteByte('"')
+ }
+}
+
+// Render writes the attributes to the given as attributes to an HTML element.
+// This is used for the default codeblock renderering.
+// This performs HTML esacaping of string attributes.
+func RenderAttributes(w hugio.FlexiWriter, skipClass bool, attributes ...Attribute) {
+ for _, attr := range attributes {
+ a := strings.ToLower(string(attr.Name))
+ if skipClass && a == "class" {
+ continue
+ }
+ _, _ = w.WriteString(" ")
+ _, _ = w.WriteString(attr.Name)
+ _, _ = w.WriteString(`="`)
+
+ switch v := attr.Value.(type) {
+ case []byte:
+ _, _ = w.Write(util.EscapeHTML(v))
+ default:
+ w.WriteString(cast.ToString(v))
+ }
+
+ _ = w.WriteByte('"')
+ }
+}
diff --git a/markup/markup.go b/markup/markup.go
index 287db7369..13e5f3042 100644
--- a/markup/markup.go
+++ b/markup/markup.go
@@ -39,11 +39,8 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
return nil, err
}
- if cfg.Highlight == nil {
- h := highlight.New(markupConfig.Highlight)
- cfg.Highlight = func(code, lang, optsStr string) (string, error) {
- return h.Highlight(code, lang, optsStr)
- }
+ if cfg.Highlighter == nil {
+ cfg.Highlighter = highlight.New(markupConfig.Highlight)
}
cfg.MarkupConfig = markupConfig
@@ -95,7 +92,7 @@ type ConverterProvider interface {
Get(name string) converter.Provider
// Default() converter.Provider
GetMarkupConfig() markup_config.Config
- Highlight(code, lang, optsStr string) (string, error)
+ GetHighlighter() highlight.Highlighter
}
type converterRegistry struct {
@@ -112,8 +109,8 @@ func (r *converterRegistry) Get(name string) converter.Provider {
return r.converters[strings.ToLower(name)]
}
-func (r *converterRegistry) Highlight(code, lang, optsStr string) (string, error) {
- return r.config.Highlight(code, lang, optsStr)
+func (r *converterRegistry) GetHighlighter() highlight.Highlighter {
+ return r.config.Highlighter
}
func (r *converterRegistry) GetMarkupConfig() markup_config.Config {
diff --git a/markup/org/convert.go b/markup/org/convert.go
index 34043e18d..603ec8f19 100644
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -27,8 +27,7 @@ import (
// Provider is the package entry point.
var Provider converter.ProviderProvider = provide{}
-type provide struct {
-}
+type provide struct{}
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
return converter.NewProvider("org", func(ctx converter.DocumentContext) (converter.Converter, error) {
diff --git a/output/layout.go b/output/layout.go
index 91c7cc652..dcbdf461a 100644
--- a/output/layout.go
+++ b/output/layout.go
@@ -31,9 +31,15 @@ var reservedSections = map[string]bool{
type LayoutDescriptor struct {
Type string
Section string
- Kind string
- Lang string
- Layout string
+
+ // E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
+ Kind string
+
+ // Comma-separated list of kind variants, e.g. "go,json" as variants which would find "render-codeblock-go.html"
+ KindVariants string
+
+ Lang string
+ Layout string
// LayoutOverride indicates what we should only look for the above layout.
LayoutOverride bool
@@ -139,6 +145,12 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
}
if d.RenderingHook {
+ if d.KindVariants != "" {
+ // Add the more specific variants first.
+ for _, variant := range strings.Split(d.KindVariants, ",") {
+ b.addLayoutVariations(d.Kind + "-" + variant)
+ }
+ }
b.addLayoutVariations(d.Kind)
b.addSectionType()
}
diff --git a/resources/page/site.go b/resources/page/site.go
index 9728df691..724f23ad7 100644
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -32,6 +32,7 @@ type Site interface {
Language() *langs.Language
RegularPages() Pages
Pages() Pages
+ Home() Page
IsServer() bool
ServerPort() int
Title() string
@@ -89,6 +90,10 @@ func (t testSite) Language() *langs.Language {
return t.l
}
+func (t testSite) Home() Page {
+ return nil
+}
+
func (t testSite) Pages() Pages {
return nil
}
diff --git a/tpl/cast/init_test.go b/tpl/cast/init_test.go
deleted file mode 100644
index 5eb4a9086..000000000
--- a/tpl/cast/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 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 cast
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/collections/init_test.go b/tpl/collections/init_test.go
deleted file mode 100644
index 570e58d93..000000000
--- a/tpl/collections/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 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 collections
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/compare/init.go b/tpl/compare/init.go
index 9aa533f55..f423f615e 100644
--- a/tpl/compare/init.go
+++ b/tpl/compare/init.go
@@ -40,14 +40,14 @@ func init() {
ns.AddMethodMapping(ctx.Eq,
[]string{"eq"},
[][2]string{
- {`{{ if eq .Section "blog" }}current{{ end }}`, `current`},
+ {`{{ if eq .Section "blog" }}current-section{{ end }}`, `current-section`},
},
)
ns.AddMethodMapping(ctx.Ge,
[]string{"ge"},
[][2]string{
- {`{{ if ge .Hugo.Version "0.36" }}Reasonable new Hugo version!{{ end }}`, `Reasonable new Hugo version!`},
+ {`{{ if ge hugo.Version "0.80" }}Reasonable new Hugo version!{{ end }}`, `Reasonable new Hugo version!`},
},
)
diff --git a/tpl/compare/init_test.go b/tpl/compare/init_test.go
deleted file mode 100644
index 8698cb5e3..000000000
--- a/tpl/compare/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 compare
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/crypto/init_test.go b/tpl/crypto/init_test.go
deleted file mode 100644
index 1c200d777..000000000
--- a/tpl/crypto/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 crypto
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/data/init_test.go b/tpl/data/init_test.go
deleted file mode 100644
index 631a91b39..000000000
--- a/tpl/data/init_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2017 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 data
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/langs"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- v := config.New()
- v.Set("contentDir", "content")
- langs.LoadLanguageSettings(v, nil)
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(newDeps(v))
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/debug/init_test.go b/tpl/debug/init_test.go
deleted file mode 100644
index 226915b34..000000000
--- a/tpl/debug/init_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2020 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 debug
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/diagrams/diagrams.go b/tpl/diagrams/diagrams.go
new file mode 100644
index 000000000..1bdbc2a02
--- /dev/null
+++ b/tpl/diagrams/diagrams.go
@@ -0,0 +1,73 @@
+// 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 diagrams
+
+import (
+ "bytes"
+ "html/template"
+ "io"
+ "strings"
+
+ "github.com/bep/goat"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/spf13/cast"
+)
+
+type SVGDiagram interface {
+ Body() template.HTML
+ SVG() template.HTML
+ Width() int
+ Height() int
+}
+
+type goatDiagram struct {
+ d goat.SVG
+}
+
+func (d goatDiagram) Body() template.HTML {
+ return template.HTML(d.d.Body)
+}
+
+func (d goatDiagram) SVG() template.HTML {
+ return template.HTML(d.d.String())
+}
+
+func (d goatDiagram) Width() int {
+ return d.d.Width
+}
+
+func (d goatDiagram) Height() int {
+ return d.d.Height
+}
+
+type Diagrams struct {
+ d *deps.Deps
+}
+
+func (d *Diagrams) Goat(v interface{}) SVGDiagram {
+ var r io.Reader
+
+ switch vv := v.(type) {
+ case io.Reader:
+ r = vv
+ case []byte:
+ r = bytes.NewReader(vv)
+ default:
+ r = strings.NewReader(cast.ToString(v))
+ }
+
+ return goatDiagram{
+ d: goat.BuildSVG(r),
+ }
+}
diff --git a/tpl/os/init_test.go b/tpl/diagrams/init.go
index 5d756bab2..1a5578837 100644
--- a/tpl/os/init_test.go
+++ b/tpl/diagrams/init.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Hugo Authors. All rights reserved.
+// 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.
@@ -11,32 +11,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package os
+package diagrams
import (
- "testing"
-
- qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/tpl/internal"
)
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
+const name = "diagrams"
+
+func init() {
+ f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+ ctx := &Diagrams{
+ d: d,
+ }
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
+ ns := &internal.TemplateFuncsNamespace{
+ Name: name,
+ Context: func(args ...interface{}) (interface{}, error) { return ctx, nil },
}
+
+ return ns
}
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
+ internal.AddTemplateFuncsNamespace(f)
}
diff --git a/tpl/encoding/init_test.go b/tpl/encoding/init_test.go
deleted file mode 100644
index 666a4e549..000000000
--- a/tpl/encoding/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 encoding
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/fmt/init_test.go b/tpl/fmt/init_test.go
deleted file mode 100644
index 07b740a73..000000000
--- a/tpl/fmt/init_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 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 fmt
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Log: loggers.NewIgnorableLogger(loggers.NewErrorLogger())})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/hugo/init_test.go b/tpl/hugo/init_test.go
deleted file mode 100644
index bc806448e..000000000
--- a/tpl/hugo/init_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 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 hugo
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/resources/page"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
- v := config.New()
- v.Set("contentDir", "content")
- s := page.NewDummyHugoSite(v)
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Site: s})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, s.Hugo())
-}
diff --git a/tpl/images/init_test.go b/tpl/images/init_test.go
deleted file mode 100644
index d8d8d7839..000000000
--- a/tpl/images/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 images
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/inflect/init_test.go b/tpl/inflect/init_test.go
deleted file mode 100644
index 38499838c..000000000
--- a/tpl/inflect/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 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 inflect
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/lang/init_test.go b/tpl/lang/init_test.go
deleted file mode 100644
index e62db95b9..000000000
--- a/tpl/lang/init_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 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 lang
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/langs"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{
- Language: langs.NewDefaultLanguage(config.New()),
- })
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/math/init_test.go b/tpl/math/init_test.go
deleted file mode 100644
index 9998eaf90..000000000
--- a/tpl/math/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 math
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/os/os.go b/tpl/os/os.go
index 43c42f5e1..8b195a527 100644
--- a/tpl/os/os.go
+++ b/tpl/os/os.go
@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
_os "os"
+ "path/filepath"
"github.com/gohugoio/hugo/deps"
"github.com/spf13/afero"
@@ -27,17 +28,9 @@ import (
// New returns a new instance of the os-namespaced template functions.
func New(d *deps.Deps) *Namespace {
- var rfs afero.Fs
- if d.Fs != nil {
- rfs = d.Fs.WorkingDir
- if d.PathSpec != nil && d.PathSpec.BaseFs != nil {
- rfs = afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(d.PathSpec.BaseFs.Content.Fs, d.Fs.WorkingDir))
- }
-
- }
-
return &Namespace{
- readFileFs: rfs,
+ readFileFs: afero.NewReadOnlyFs(afero.NewCopyOnWriteFs(d.PathSpec.BaseFs.Content.Fs, d.PathSpec.BaseFs.Work)),
+ workFs: d.PathSpec.BaseFs.Work,
deps: d,
}
}
@@ -45,6 +38,7 @@ func New(d *deps.Deps) *Namespace {
// Namespace provides template functions for the "os" namespace.
type Namespace struct {
readFileFs afero.Fs
+ workFs afero.Fs
deps *deps.Deps
}
@@ -66,8 +60,9 @@ func (ns *Namespace) Getenv(key interface{}) (string, error) {
// readFile reads the file named by filename in the given filesystem
// and returns the contents as a string.
func readFile(fs afero.Fs, filename string) (string, error) {
- if filename == "" {
- return "", errors.New("readFile needs a filename")
+ filename = filepath.Clean(filename)
+ if filename == "" || filename == "." || filename == string(_os.PathSeparator) {
+ return "", errors.New("invalid filename")
}
b, err := afero.ReadFile(fs, filename)
@@ -101,7 +96,7 @@ func (ns *Namespace) ReadDir(i interface{}) ([]_os.FileInfo, error) {
return nil, err
}
- list, err := afero.ReadDir(ns.deps.Fs.WorkingDir, path)
+ list, err := afero.ReadDir(ns.workFs, path)
if err != nil {
return nil, fmt.Errorf("failed to read directory %q: %s", path, err)
}
diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go
index bbc0d018c..59491e97c 100644
--- a/tpl/os/os_test.go
+++ b/tpl/os/os_test.go
@@ -11,34 +11,26 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package os
+package os_test
import (
"path/filepath"
"testing"
- "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/tpl/os"
qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/spf13/afero"
)
func TestReadFile(t *testing.T) {
t.Parallel()
- c := qt.New(t)
-
- workingDir := "/home/hugo"
- v := config.New()
- v.Set("workingDir", workingDir)
+ b := newFileTestBuilder(t).Build()
- // f := newTestFuncsterWithViper(v)
- ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
+ // helpers.PrintFs(b.H.PathSpec.BaseFs.Work, "", _os.Stdout)
- afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
- afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755)
+ ns := os.New(b.H.Deps)
for _, test := range []struct {
filename string
@@ -53,13 +45,13 @@ func TestReadFile(t *testing.T) {
result, err := ns.ReadFile(test.filename)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
@@ -67,15 +59,8 @@ func TestFileExists(t *testing.T) {
t.Parallel()
c := qt.New(t)
- workingDir := "/home/hugo"
-
- v := config.New()
- v.Set("workingDir", workingDir)
-
- ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
-
- afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
- afero.WriteFile(ns.deps.Fs.Source, filepath.Join("/home", "f2.txt"), []byte("f2-content"), 0755)
+ b := newFileTestBuilder(t).Build()
+ ns := os.New(b.H.Deps)
for _, test := range []struct {
filename string
@@ -101,15 +86,8 @@ func TestFileExists(t *testing.T) {
func TestStat(t *testing.T) {
t.Parallel()
- c := qt.New(t)
- workingDir := "/home/hugo"
-
- v := config.New()
- v.Set("workingDir", workingDir)
-
- ns := New(&deps.Deps{Fs: hugofs.NewMem(v)})
-
- afero.WriteFile(ns.deps.Fs.Source, filepath.Join(workingDir, "/f/f1.txt"), []byte("f1-content"), 0755)
+ b := newFileTestBuilder(t).Build()
+ ns := os.New(b.H.Deps)
for _, test := range []struct {
filename string
@@ -123,11 +101,28 @@ func TestStat(t *testing.T) {
result, err := ns.Stat(test.filename)
if test.expect == nil {
- c.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result.Size(), qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result.Size(), qt.Equals, test.expect)
}
}
+
+func newFileTestBuilder(t *testing.T) *hugolib.IntegrationTestBuilder {
+ files := `
+-- f/f1.txt --
+f1-content
+-- home/f2.txt --
+f2-content
+ `
+
+ return hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ WorkingDir: "/mywork",
+ },
+ )
+}
diff --git a/tpl/partials/init_test.go b/tpl/partials/init_test.go
deleted file mode 100644
index b18ac0a00..000000000
--- a/tpl/partials/init_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright 2017 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 partials
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{
- BuildStartListeners: &deps.Listeners{},
- Log: loggers.NewErrorLogger(),
- })
- if ns.Name == namespaceName {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/path/init_test.go b/tpl/path/init_test.go
deleted file mode 100644
index 2282c3305..000000000
--- a/tpl/path/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2018 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 path
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/reflect/init_test.go b/tpl/reflect/init_test.go
deleted file mode 100644
index 2ad33fc25..000000000
--- a/tpl/reflect/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 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 reflect
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/safe/init_test.go b/tpl/safe/init_test.go
deleted file mode 100644
index 7aa1473d2..000000000
--- a/tpl/safe/init_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 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 safe
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
-
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/site/init_test.go b/tpl/site/init_test.go
deleted file mode 100644
index 46af2ef5b..000000000
--- a/tpl/site/init_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 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 site
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/resources/page"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
-
- var found bool
- var ns *internal.TemplateFuncsNamespace
- v := config.New()
- v.Set("contentDir", "content")
- s := page.NewDummyHugoSite(v)
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Site: s})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, s)
-}
diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go
deleted file mode 100644
index 39d928601..000000000
--- a/tpl/strings/init_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2017 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 strings
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Cfg: config.New()})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/templates/init_test.go b/tpl/templates/init_test.go
deleted file mode 100644
index ada53b185..000000000
--- a/tpl/templates/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2018 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 templates
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/time/init_test.go b/tpl/time/init_test.go
deleted file mode 100644
index d7efabfa7..000000000
--- a/tpl/time/init_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2017 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 time
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/langs"
-
- "github.com/gohugoio/hugo/htesting/hqt"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{
- Language: langs.NewDefaultLanguage(config.New()),
- })
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html b/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
new file mode 100644
index 000000000..7c2b99f3f
--- /dev/null
+++ b/tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html
@@ -0,0 +1 @@
+adf
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 44b486404..706719278 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -281,15 +281,10 @@ func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
for _, ts := range t.main.templates {
ti := ts.info
- if strings.HasPrefix(ti.name, "_internal/") {
- continue
- }
- if strings.HasPrefix(ti.name, "partials/inline/pagination") {
- // TODO(bep) we need to fix this. These are internal partials, but
- // they may also be defined in the project, which currently could
- // lead to some false negatives.
+ if strings.HasPrefix(ti.name, "_internal/") || ti.realFilename == "" {
continue
}
+
if _, found := t.templateUsageTracker[ti.name]; !found {
unused = append(unused, ti)
}
@@ -740,6 +735,7 @@ func (t *templateHandler) extractIdentifiers(line string) []string {
}
//go:embed embedded/templates/*
+//go:embed embedded/templates/_default/*
var embededTemplatesFs embed.FS
func (t *templateHandler) loadEmbedded() error {
@@ -757,9 +753,19 @@ func (t *templateHandler) loadEmbedded() error {
// to write the templates to Go files.
templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
+ templateName := name
- if err := t.AddTemplate(internalPathPrefix+name, templ); err != nil {
- return err
+ // For the render hooks it does not make sense to preseve the
+ // double _indternal double book-keeping,
+ // just add it if its now provided by the user.
+ if !strings.Contains(path, "_default/_markup") {
+ templateName = internalPathPrefix + name
+ }
+
+ if _, found := t.Lookup(templateName); !found {
+ if err := t.AddTemplate(templateName, templ); err != nil {
+ return err
+ }
}
if aliases, found := embeddedTemplatesAliases[name]; found {
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index 831b846d0..8692b9ee2 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -38,6 +38,7 @@ import (
_ "github.com/gohugoio/hugo/tpl/crypto"
_ "github.com/gohugoio/hugo/tpl/data"
_ "github.com/gohugoio/hugo/tpl/debug"
+ _ "github.com/gohugoio/hugo/tpl/diagrams"
_ "github.com/gohugoio/hugo/tpl/encoding"
_ "github.com/gohugoio/hugo/tpl/fmt"
_ "github.com/gohugoio/hugo/tpl/hugo"
diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go
index 6d2587bf7..cb1aa6feb 100644
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -11,223 +11,74 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package tplimpl
+package tplimpl_test
import (
- "bytes"
- "context"
"fmt"
- "path/filepath"
- "reflect"
+ "strings"
"testing"
- "time"
- "github.com/gohugoio/hugo/modules"
+ "github.com/gohugoio/hugo/hugolib"
- "github.com/gohugoio/hugo/resources/page"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/common/hugo"
- "github.com/gohugoio/hugo/common/loggers"
- "github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/langs"
- "github.com/gohugoio/hugo/langs/i18n"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/internal"
- "github.com/gohugoio/hugo/tpl/partials"
- "github.com/spf13/afero"
)
-var logger = loggers.NewErrorLogger()
-
-func newTestConfig() config.Provider {
- v := config.New()
- v.Set("contentDir", "content")
- v.Set("dataDir", "data")
- v.Set("i18nDir", "i18n")
- v.Set("layoutDir", "layouts")
- v.Set("archetypeDir", "archetypes")
- v.Set("assetDir", "assets")
- v.Set("resourceDir", "resources")
- v.Set("publishDir", "public")
-
- langs.LoadLanguageSettings(v, nil)
- mod, err := modules.CreateProjectModule(v)
- if err != nil {
- panic(err)
- }
- v.Set("allModules", modules.Modules{mod})
-
- return v
-}
-
-func newDepsConfig(cfg config.Provider) deps.DepsCfg {
- l := langs.NewLanguage("en", cfg)
- return deps.DepsCfg{
- Language: l,
- Site: page.NewDummyHugoSite(cfg),
- Cfg: cfg,
- Fs: hugofs.NewMem(l),
- Logger: logger,
- TemplateProvider: DefaultTemplateProvider,
- TranslationProvider: i18n.NewTranslationProvider(),
- }
-}
-
func TestTemplateFuncsExamples(t *testing.T) {
t.Parallel()
- c := qt.New(t)
-
- workingDir := "/home/hugo"
-
- v := newTestConfig()
-
- v.Set("workingDir", workingDir)
- v.Set("multilingual", true)
- v.Set("contentDir", "content")
- v.Set("assetDir", "assets")
- v.Set("baseURL", "http://mysite.com/hugo/")
- v.Set("CurrentContentLanguage", langs.NewLanguage("en", v))
-
- fs := hugofs.NewMem(v)
- afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "README.txt"), []byte("Hugo Rocks!"), 0755)
-
- depsCfg := newDepsConfig(v)
- depsCfg.Fs = fs
- d, err := deps.New(depsCfg)
- defer d.Close()
- c.Assert(err, qt.IsNil)
-
- var data struct {
- Title string
- Section string
- Hugo map[string]interface{}
- Params map[string]interface{}
- }
-
- data.Title = "**BatMan**"
- data.Section = "blog"
- data.Params = map[string]interface{}{"langCode": "en"}
- data.Hugo = map[string]interface{}{"Version": hugo.MustParseVersion("0.36.1").Version()}
+ files := `
+-- config.toml --
+disableKinds=["home", "section", "taxonomy", "term", "sitemap", "robotsTXT"]
+ignoreErrors = ["my-err-id"]
+[outputs]
+home=["HTML"]
+-- layouts/partials/header.html --
+<title>Hugo Rocks!</title>
+-- files/README.txt --
+Hugo Rocks!
+-- content/blog/hugo-rocks.md --
+---
+title: "**BatMan**"
+---
+`
+
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ },
+ ).Build()
+
+ d := b.H.Sites[0].Deps
+
+ var (
+ templates []string
+ expected []string
+ )
for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns := nsf(d)
for _, mm := range ns.MethodMappings {
- for i, example := range mm.Examples {
- in, expected := example[0], example[1]
- d.WithTemplate = func(templ tpl.TemplateManager) error {
- c.Assert(templ.AddTemplate("test", in), qt.IsNil)
- c.Assert(templ.AddTemplate("partials/header.html", "<title>Hugo Rocks!</title>"), qt.IsNil)
- return nil
- }
- c.Assert(d.LoadResources(), qt.IsNil)
-
- var b bytes.Buffer
- templ, _ := d.Tmpl().Lookup("test")
- c.Assert(d.Tmpl().Execute(templ, &b, &data), qt.IsNil)
- if b.String() != expected {
- t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected)
+ for _, example := range mm.Examples {
+ if strings.Contains(example[0], "errorf") {
+ // This will fail the build, so skip for now.
+ continue
}
+ templates = append(templates, example[0])
+ expected = append(expected, example[1])
}
}
}
-}
-
-// TODO(bep) it would be dandy to put this one into the partials package, but
-// we have some package cycle issues to solve first.
-func TestPartialCached(t *testing.T) {
- t.Parallel()
-
- c := qt.New(t)
-
- partial := `Now: {{ now.UnixNano }}`
- name := "testing"
-
- var data struct{}
-
- v := newTestConfig()
-
- config := newDepsConfig(v)
-
- config.WithTemplate = func(templ tpl.TemplateManager) error {
- err := templ.AddTemplate("partials/"+name, partial)
- if err != nil {
- return err
- }
-
- return nil
- }
-
- de, err := deps.New(config)
- c.Assert(err, qt.IsNil)
- defer de.Close()
- c.Assert(de.LoadResources(), qt.IsNil)
-
- ns := partials.New(de)
- res1, err := ns.IncludeCached(context.Background(), name, &data)
- c.Assert(err, qt.IsNil)
+ files += fmt.Sprintf("-- layouts/_default/single.html --\n%s\n", strings.Join(templates, "\n"))
+ b = hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{
+ T: t,
+ TxtarString: files,
+ NeedsOsFS: true,
+ },
+ ).Build()
- for j := 0; j < 10; j++ {
- time.Sleep(2 * time.Nanosecond)
- res2, err := ns.IncludeCached(context.Background(), name, &data)
- c.Assert(err, qt.IsNil)
-
- if !reflect.DeepEqual(res1, res2) {
- t.Fatalf("cache mismatch")
- }
-
- res3, err := ns.IncludeCached(context.Background(), name, &data, fmt.Sprintf("variant%d", j))
- c.Assert(err, qt.IsNil)
-
- if reflect.DeepEqual(res1, res3) {
- t.Fatalf("cache mismatch")
- }
- }
-}
-
-func BenchmarkPartial(b *testing.B) {
- doBenchmarkPartial(b, func(ns *partials.Namespace) error {
- _, err := ns.Include(context.Background(), "bench1")
- return err
- })
-}
-
-func BenchmarkPartialCached(b *testing.B) {
- doBenchmarkPartial(b, func(ns *partials.Namespace) error {
- _, err := ns.IncludeCached(context.Background(), "bench1", nil)
- return err
- })
-}
-
-func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) {
- c := qt.New(b)
- config := newDepsConfig(config.New())
- config.WithTemplate = func(templ tpl.TemplateManager) error {
- err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`)
- if err != nil {
- return err
- }
-
- return nil
- }
-
- de, err := deps.New(config)
- c.Assert(err, qt.IsNil)
- defer de.Close()
- c.Assert(de.LoadResources(), qt.IsNil)
-
- ns := partials.New(de)
-
- b.ResetTimer()
- b.RunParallel(func(pb *testing.PB) {
- for pb.Next() {
- if err := f(ns); err != nil {
- b.Fatalf("error executing template: %s", err)
- }
- }
- })
+ b.AssertFileContent("public/blog/hugo-rocks/index.html", expected...)
}
diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go
deleted file mode 100644
index eaf57166a..000000000
--- a/tpl/tplimpl/template_info_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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 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 tplimpl
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/hugofs"
- "github.com/gohugoio/hugo/tpl"
-)
-
-func TestTemplateInfoShortcode(t *testing.T) {
- c := qt.New(t)
- d := newD(c)
- defer d.Close()
- h := d.Tmpl().(*templateExec)
-
- c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
-{{ .Inner }}
-`), qt.IsNil)
-
- c.Assert(h.postTransform(), qt.IsNil)
-
- tt, found, _ := d.Tmpl().LookupVariant("mytemplate", tpl.TemplateVariants{})
-
- c.Assert(found, qt.Equals, true)
- tti, ok := tt.(tpl.Info)
- c.Assert(ok, qt.Equals, true)
- c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
-}
-
-// TODO(bep) move and use in other places
-func newD(c *qt.C) *deps.Deps {
- v := newTestConfig()
- fs := hugofs.NewMem(v)
-
- depsCfg := newDepsConfig(v)
- depsCfg.Fs = fs
- d, err := deps.New(depsCfg)
- c.Assert(err, qt.IsNil)
-
- provider := DefaultTemplateProvider
- provider.Update(d)
-
- return d
-}
diff --git a/tpl/transform/init_test.go b/tpl/transform/init_test.go
deleted file mode 100644
index ec3c35897..000000000
--- a/tpl/transform/init_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright 2017 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 transform
-
-import (
- "testing"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-}
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
index 8e94ef6bf..22548593b 100644
--- a/tpl/transform/remarshal_test.go
+++ b/tpl/transform/remarshal_test.go
@@ -11,13 +11,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package transform
+package transform_test
import (
"testing"
- "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/htesting"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/tpl/transform"
qt "github.com/frankban/quicktest"
)
@@ -25,13 +26,14 @@ import (
func TestRemarshal(t *testing.T) {
t.Parallel()
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ ns := transform.New(b.H.Deps)
c := qt.New(t)
c.Run("Roundtrip variants", func(c *qt.C) {
-
tomlExample := `title = 'Test Metadata'
[[resources]]
@@ -129,7 +131,6 @@ title: Test Metadata
}
}
-
})
c.Run("Comments", func(c *qt.C) {
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 8ea91f234..dc7cc0342 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -19,6 +19,9 @@ import (
"html/template"
"github.com/gohugoio/hugo/cache/namedmemcache"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/markup/converter/hooks"
+ "github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
@@ -65,18 +68,28 @@ func (ns *Namespace) Highlight(s interface{}, lang string, opts ...interface{})
return "", err
}
- sopts := ""
+ var optsv interface{}
if len(opts) > 0 {
- sopts, err = cast.ToStringE(opts[0])
- if err != nil {
- return "", err
- }
+ optsv = opts[0]
}
- highlighted, _ := ns.deps.ContentSpec.Converters.Highlight(ss, lang, sopts)
+ hl := ns.deps.ContentSpec.Converters.GetHighlighter()
+ highlighted, _ := hl.Highlight(ss, lang, optsv)
return template.HTML(highlighted), nil
}
+// HighlightCodeBlock highlights a code block on the form received in the codeblock render hooks.
+func (ns *Namespace) HighlightCodeBlock(ctx hooks.CodeblockContext, opts ...interface{}) (highlight.HightlightResult, error) {
+ var optsv interface{}
+ if len(opts) > 0 {
+ optsv = opts[0]
+ }
+
+ hl := ns.deps.ContentSpec.Converters.GetHighlighter()
+
+ return hl.HighlightCodeBlock(ctx, optsv)
+}
+
// HTMLEscape returns a copy of s with reserved HTML characters escaped.
func (ns *Namespace) HTMLEscape(s interface{}) (string, error) {
ss, err := cast.ToStringE(s)
@@ -100,20 +113,22 @@ func (ns *Namespace) HTMLUnescape(s interface{}) (string, error) {
// Markdownify renders a given input from Markdown to HTML.
func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
+ defer herrors.Recover()
ss, err := cast.ToStringE(s)
if err != nil {
return "", err
}
- b, err := ns.deps.ContentSpec.RenderMarkdown([]byte(ss))
- if err != nil {
- return "", err
+ home := ns.deps.Site.Home()
+ if home == nil {
+ panic("home must not be nil")
}
+ sss, err := home.RenderString(ss)
// Strip if this is a short inline type of text.
- b = ns.deps.ContentSpec.TrimShortHTML(b)
+ bb := ns.deps.ContentSpec.TrimShortHTML([]byte(sss))
- return helpers.BytesToHTML(b), nil
+ return helpers.BytesToHTML(bb), nil
}
// Plainify returns a copy of s with all HTML tags removed.
@@ -125,3 +140,7 @@ func (ns *Namespace) Plainify(s interface{}) (string, error) {
return helpers.StripHTML(ss), nil
}
+
+func (ns *Namespace) Reset() {
+ ns.cache.Clear()
+}
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index 260de5f83..3ccf1a270 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -11,13 +11,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package transform
+package transform_test
import (
"html/template"
"testing"
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/tpl/transform"
"github.com/spf13/afero"
qt "github.com/frankban/quicktest"
@@ -32,10 +34,11 @@ type tstNoStringer struct{}
func TestEmojify(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -49,23 +52,23 @@ func TestEmojify(t *testing.T) {
result, err := ns.Emojify(test.s)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
func TestHighlight(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -82,23 +85,23 @@ func TestHighlight(t *testing.T) {
result, err := ns.Highlight(test.s, test.lang, test.opts)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(string(result), qt.Contains, test.expect.(string))
+ b.Assert(err, qt.IsNil)
+ b.Assert(string(result), qt.Contains, test.expect.(string))
}
}
func TestHTMLEscape(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -112,23 +115,23 @@ func TestHTMLEscape(t *testing.T) {
result, err := ns.HTMLEscape(test.s)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
func TestHTMLUnescape(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -142,23 +145,23 @@ func TestHTMLUnescape(t *testing.T) {
result, err := ns.HTMLUnescape(test.s)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
func TestMarkdownify(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -171,23 +174,24 @@ func TestMarkdownify(t *testing.T) {
result, err := ns.Markdownify(test.s)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
// Issue #3040
func TestMarkdownifyBlocksOfText(t *testing.T) {
t.Parallel()
- c := qt.New(t)
- v := config.New()
- v.Set("contentDir", "content")
- ns := New(newDeps(v))
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ ns := transform.New(b.H.Deps)
text := `
#First
@@ -202,17 +206,18 @@ And then some.
`
result, err := ns.Markdownify(text)
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, template.HTML(
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, template.HTML(
"<p>#First</p>\n<p>This is some <em>bold</em> text.</p>\n<h2 id=\"second\">Second</h2>\n<p>This is some more text.</p>\n<p>And then some.</p>\n"))
}
func TestPlainify(t *testing.T) {
t.Parallel()
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
- v := config.New()
- ns := New(newDeps(v))
+ ns := transform.New(b.H.Deps)
for _, test := range []struct {
s interface{}
@@ -225,13 +230,13 @@ func TestPlainify(t *testing.T) {
result, err := ns.Plainify(test.s)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
continue
}
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go
index fb0e446c3..2b14282ec 100644
--- a/tpl/transform/unmarshal_test.go
+++ b/tpl/transform/unmarshal_test.go
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package transform
+package transform_test
import (
"fmt"
@@ -19,7 +19,8 @@ import (
"strings"
"testing"
- "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/tpl/transform"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/resources/resource"
@@ -80,12 +81,14 @@ func (t testContentResource) Key() string {
}
func TestUnmarshal(t *testing.T) {
- v := config.New()
- ns := New(newDeps(v))
- c := qt.New(t)
+ b := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: t},
+ ).Build()
+
+ ns := transform.New(b.H.Deps)
assertSlogan := func(m map[string]interface{}) {
- c.Assert(m["slogan"], qt.Equals, "Hugo Rocks!")
+ b.Assert(m["slogan"], qt.Equals, "Hugo Rocks!")
}
for _, test := range []struct {
@@ -116,24 +119,24 @@ func TestUnmarshal(t *testing.T) {
}},
{testContentResource{key: "r1", content: `1997,Ford,E350,"ac, abs, moon",3000.00
1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.CSVType}, nil, func(r [][]string) {
- c.Assert(len(r), qt.Equals, 2)
+ b.Assert(len(r), qt.Equals, 2)
first := r[0]
- c.Assert(len(first), qt.Equals, 5)
- c.Assert(first[1], qt.Equals, "Ford")
+ b.Assert(len(first), qt.Equals, 5)
+ b.Assert(first[1], qt.Equals, "Ford")
}},
{testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
- c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+ b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
}},
{"a,b,c", nil, func(r [][]string) {
- c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+ b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
}},
{"a;b;c", map[string]interface{}{"delimiter": ";"}, func(r [][]string) {
- c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+ b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
}},
{testContentResource{key: "r1", content: `
% This is a comment
a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment": "%"}, func(r [][]string) {
- c.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
+ b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r)
}},
// errors
{"thisisnotavaliddataformat", nil, false},
@@ -144,7 +147,7 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
{tstNoStringer{}, nil, false},
} {
- ns.cache.Clear()
+ ns.Reset()
var args []interface{}
@@ -156,29 +159,32 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
result, err := ns.Unmarshal(args...)
- if b, ok := test.expect.(bool); ok && !b {
- c.Assert(err, qt.Not(qt.IsNil))
+ if bb, ok := test.expect.(bool); ok && !bb {
+ b.Assert(err, qt.Not(qt.IsNil))
} else if fn, ok := test.expect.(func(m map[string]interface{})); ok {
- c.Assert(err, qt.IsNil)
+ b.Assert(err, qt.IsNil)
m, ok := result.(map[string]interface{})
- c.Assert(ok, qt.Equals, true)
+ b.Assert(ok, qt.Equals, true)
fn(m)
} else if fn, ok := test.expect.(func(r [][]string)); ok {
- c.Assert(err, qt.IsNil)
+ b.Assert(err, qt.IsNil)
r, ok := result.([][]string)
- c.Assert(ok, qt.Equals, true)
+ b.Assert(ok, qt.Equals, true)
fn(r)
} else {
- c.Assert(err, qt.IsNil)
- c.Assert(result, qt.Equals, test.expect)
+ b.Assert(err, qt.IsNil)
+ b.Assert(result, qt.Equals, test.expect)
}
}
}
func BenchmarkUnmarshalString(b *testing.B) {
- v := config.New()
- ns := New(newDeps(v))
+ bb := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: b},
+ ).Build()
+
+ ns := transform.New(bb.H.Deps)
const numJsons = 100
@@ -200,8 +206,11 @@ func BenchmarkUnmarshalString(b *testing.B) {
}
func BenchmarkUnmarshalResource(b *testing.B) {
- v := config.New()
- ns := New(newDeps(v))
+ bb := hugolib.NewIntegrationTestBuilder(
+ hugolib.IntegrationTestConfig{T: b},
+ ).Build()
+
+ ns := transform.New(bb.H.Deps)
const numJsons = 100
diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go
deleted file mode 100644
index 7e53c247a..000000000
--- a/tpl/urls/init_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2017 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 urls
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/config"
-
- qt "github.com/frankban/quicktest"
- "github.com/gohugoio/hugo/deps"
- "github.com/gohugoio/hugo/htesting/hqt"
- "github.com/gohugoio/hugo/tpl/internal"
-)
-
-func TestInit(t *testing.T) {
- c := qt.New(t)
- var found bool
- var ns *internal.TemplateFuncsNamespace
-
- for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
- ns = nsf(&deps.Deps{Cfg: config.New()})
- if ns.Name == name {
- found = true
- break
- }
- }
-
- c.Assert(found, qt.Equals, true)
- ctx, err := ns.Context()
- c.Assert(err, qt.IsNil)
- c.Assert(ctx, hqt.IsSameType, &Namespace{})
-
-}