hugo

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

git clone git://git.shimmy1996.com/hugo.git
commit 08fdca9d9365eaf1e496a12e2af5e18617bd0e66
parent 2c20f5bc00b604e72b3b7e401fbdbf9447fe3470
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Thu, 17 Feb 2022 13:04:00 +0100

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

Diffstat:
Mcommon/hugio/writers.go | 8++++++++
Mdocs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/_default/baseof.html | 8++++++++
Adocs/content/en/content-management/diagrams.md | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adocs/layouts/_default/_markup/render-codeblock-goat.html | 18++++++++++++++++++
Adocs/layouts/_default/_markup/render-codeblock-mermaid.html | 4++++
Mgo.mod | 5+++--
Mgo.sum | 8++++++++
Mhelpers/content.go | 12++----------
Mhugolib/content_render_hooks_test.go | 4++--
Mhugolib/integrationtest_builder.go | 4++--
Mhugolib/language_content_dir_test.go | 2+-
Mhugolib/page.go | 64+++++++++++-----------------------------------------------------
Mhugolib/page__new.go | 5++++-
Mhugolib/page__per_output.go | 173+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mhugolib/page_test.go | 5++---
Mhugolib/pagebundler_test.go | 4++--
Mhugolib/site.go | 16++++++++++------
Mhugolib/site_sections.go | 10+++-------
Mmarkup/converter/converter.go | 10++++++----
Mmarkup/converter/hooks/hooks.go | 100+++++++++++++++++++++++++------------------------------------------------------
Amarkup/goldmark/codeblocks/integration_test.go | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkup/goldmark/codeblocks/render.go | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amarkup/goldmark/codeblocks/transform.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmarkup/goldmark/convert.go | 146+++++++------------------------------------------------------------------------
Mmarkup/goldmark/convert_test.go | 25++++++++++++++++++++++---
Mmarkup/goldmark/integration_test.go | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Amarkup/goldmark/internal/render/context.go | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmarkup/goldmark/render_hooks.go | 143++++++++++++++++++++++++++-----------------------------------------------------
Mmarkup/goldmark/toc_test.go | 9++++++---
Mmarkup/highlight/config.go | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mmarkup/highlight/highlight.go | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Amarkup/internal/attributes/attributes.go | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmarkup/markup.go | 13+++++--------
Mmarkup/org/convert.go | 3+--
Moutput/layout.go | 18+++++++++++++++---
Mresources/page/site.go | 5+++++
Dtpl/cast/init_test.go | 43-------------------------------------------
Dtpl/collections/init_test.go | 43-------------------------------------------
Mtpl/compare/init.go | 4++--
Dtpl/compare/init_test.go | 42------------------------------------------
Dtpl/crypto/init_test.go | 42------------------------------------------
Dtpl/data/init_test.go | 47-----------------------------------------------
Dtpl/debug/init_test.go | 44--------------------------------------------
Atpl/diagrams/diagrams.go | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atpl/diagrams/init.go | 38++++++++++++++++++++++++++++++++++++++
Dtpl/encoding/init_test.go | 42------------------------------------------
Dtpl/fmt/init_test.go | 44--------------------------------------------
Dtpl/hugo/init_test.go | 49-------------------------------------------------
Dtpl/images/init_test.go | 42------------------------------------------
Dtpl/inflect/init_test.go | 43-------------------------------------------
Dtpl/lang/init_test.go | 48------------------------------------------------
Dtpl/math/init_test.go | 42------------------------------------------
Dtpl/os/init_test.go | 42------------------------------------------
Mtpl/os/os.go | 21++++++++-------------
Mtpl/os/os_test.go | 73++++++++++++++++++++++++++++++++++---------------------------------------
Dtpl/partials/init_test.go | 46----------------------------------------------
Dtpl/path/init_test.go | 43-------------------------------------------
Dtpl/reflect/init_test.go | 43-------------------------------------------
Dtpl/safe/init_test.go | 43-------------------------------------------
Dtpl/site/init_test.go | 49-------------------------------------------------
Dtpl/strings/init_test.go | 45---------------------------------------------
Dtpl/templates/init_test.go | 42------------------------------------------
Dtpl/time/init_test.go | 48------------------------------------------------
Atpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html | 1+
Mtpl/tplimpl/template.go | 24+++++++++++++++---------
Mtpl/tplimpl/template_funcs.go | 1+
Mtpl/tplimpl/template_funcs_test.go | 245++++++++++++++++---------------------------------------------------------------
Dtpl/tplimpl/template_info_test.go | 58----------------------------------------------------------
Dtpl/transform/init_test.go | 42------------------------------------------
Mtpl/transform/remarshal_test.go | 15++++++++-------
Mtpl/transform/transform.go | 41++++++++++++++++++++++++++++++-----------
Mtpl/transform/transform_test.go | 111+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtpl/transform/unmarshal_test.go | 61+++++++++++++++++++++++++++++++++++--------------------------
Dtpl/urls/init_test.go | 45---------------------------------------------
74 files changed, 1905 insertions(+), 2004 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
+<div class="mermaid">
+  {{- .Code | safeHTML }}
+</div>
+{{ .Page.Store.Set "hasMermaid" true }}
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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/diagrams/init.go b/tpl/diagrams/init.go
@@ -0,0 +1,38 @@
+// 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 (
+	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/tpl/internal"
+)
+
+const name = "diagrams"
+
+func init() {
+	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+		ctx := &Diagrams{
+			d: d,
+		}
+
+		ns := &internal.TemplateFuncsNamespace{
+			Name:    name,
+			Context: func(args ...interface{}) (interface{}, error) { return ctx, nil },
+		}
+
+		return ns
+	}
+
+	internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/encoding/init_test.go b/tpl/encoding/init_test.go
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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/init_test.go b/tpl/os/init_test.go
@@ -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 os
-
-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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1 @@
+adf
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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{})
-
-}