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
This commit is contained in:
Bjørn Erik Pedersen 2022-02-17 13:04:00 +01:00
parent 2c20f5bc00
commit 08fdca9d93
73 changed files with 1887 additions and 1986 deletions

View file

@ -18,6 +18,14 @@ import (
"io/ioutil" "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 { type multiWriteCloser struct {
io.Writer io.Writer
closers []io.WriteCloser closers []io.WriteCloser

View file

@ -66,6 +66,14 @@
{{ block "footer" . }}{{ partialCached "site-footer.html" . }}{{ end }} {{ 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> </body>
</html> </html>

View file

@ -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 } """" .│
└────────────────────────────────────────────────┘
```

View file

@ -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>

View file

@ -0,0 +1,4 @@
<div class="mermaid">
{{- .Code | safeHTML }}
</div>
{{ .Page.Store.Set "hasMermaid" true }}

5
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go v1.43.5 github.com/aws/aws-sdk-go v1.43.5
github.com/bep/debounce v1.2.0 github.com/bep/debounce v1.2.0
github.com/bep/gitmap v1.1.2 github.com/bep/gitmap v1.1.2
github.com/bep/goat v0.5.0
github.com/bep/godartsass v0.12.0 github.com/bep/godartsass v0.12.0
github.com/bep/golibsass v1.0.0 github.com/bep/golibsass v1.0.0
github.com/bep/gowebp v0.1.0 github.com/bep/gowebp v0.1.0
@ -19,7 +20,7 @@ require (
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/evanw/esbuild v0.14.22 github.com/evanw/esbuild v0.14.22
github.com/fortytw2/leaktest v1.3.0 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/fsnotify/fsnotify v1.5.1
github.com/getkin/kin-openapi v0.85.0 github.com/getkin/kin-openapi v0.85.0
github.com/ghodss/yaml v1.0.0 github.com/ghodss/yaml v1.0.0
@ -57,7 +58,7 @@ require (
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/tdewolff/minify/v2 v2.9.29 github.com/tdewolff/minify/v2 v2.9.29
github.com/yuin/goldmark v1.4.7 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 gocloud.dev v0.20.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd

8
go.sum
View file

@ -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/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840=
github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= 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 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E=
github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4= github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4=
github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw= 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.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 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.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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 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.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 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.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/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8= gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8=

View file

@ -30,6 +30,7 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup" "github.com/gohugoio/hugo/markup"
@ -47,8 +48,8 @@ var (
// ContentSpec provides functionality to render markdown content. // ContentSpec provides functionality to render markdown content.
type ContentSpec struct { type ContentSpec struct {
Converters markup.ConverterProvider Converters markup.ConverterProvider
MardownConverter converter.Converter // Markdown converter with no document context
anchorNameSanitizer converter.AnchorNameSanitizer 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 is the length of the summary that Hugo extracts from a content.
summaryLength int summaryLength int
@ -88,7 +89,6 @@ func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.
if err != nil { if err != nil {
return nil, err return nil, err
} }
spec.MardownConverter = conv
if as, ok := conv.(converter.AnchorNameSanitizer); ok { if as, ok := conv.(converter.AnchorNameSanitizer); ok {
spec.anchorNameSanitizer = as spec.anchorNameSanitizer = as
} else { } else {
@ -192,14 +192,6 @@ func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
return 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 { func (c *ContentSpec) SanitizeAnchorName(s string) string {
return c.anchorNameSanitizer.SanitizeAnchorName(s) return c.anchorNameSanitizer.SanitizeAnchorName(s)
} }

View file

@ -231,8 +231,8 @@ SHORT3|
b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`) 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`) // 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>`) 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. // markdownify
b.AssertFileContent("public/blog/p5/index.html", "Inner Link: <a href=\"https://www.google.com\" title=\"Google's Homepage\">Inner Link</a>\n</div>") b.AssertFileContent("public/blog/p5/index.html", "Inner Link: |https://www.google.com|Title: Google's Homepage|Text: Inner Link|END")
b.AssertFileContent("public/blog/p6/index.html", b.AssertFileContent("public/blog/p6/index.html",
"Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END", "Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END",

View file

@ -125,7 +125,7 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
if match == "" || strings.HasPrefix(match, "#") { if match == "" || strings.HasPrefix(match, "#") {
continue 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 { func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
s.Helper() s.Helper()
_, err := s.BuildE() _, err := s.BuildE()
if s.Cfg.Verbose { if s.Cfg.Verbose || err != nil {
fmt.Println(s.logBuff.String()) fmt.Println(s.logBuff.String())
} }
s.Assert(err, qt.IsNil) s.Assert(err, qt.IsNil)

View file

@ -314,7 +314,7 @@ Content.
nnSect := nnSite.getPage(page.KindSection, "sect") nnSect := nnSite.getPage(page.KindSection, "sect")
c.Assert(nnSect, qt.Not(qt.IsNil)) c.Assert(nnSect, qt.Not(qt.IsNil))
c.Assert(len(nnSect.Pages()), qt.Equals, 12) c.Assert(len(nnSect.Pages()), qt.Equals, 12)
nnHome, _ := nnSite.Info.Home() nnHome := nnSite.Info.Home()
c.Assert(nnHome.RelPermalink(), qt.Equals, "/nn/") c.Assert(nnHome.RelPermalink(), qt.Equals, "/nn/")
} }

View file

@ -22,6 +22,8 @@ import (
"sort" "sort"
"strings" "strings"
"go.uber.org/atomic"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
@ -47,7 +49,6 @@ import (
"github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
@ -118,6 +119,9 @@ type pageState struct {
// formats (for all sites). // formats (for all sites).
pageOutputs []*pageOutput 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. // This will be shifted out when we start to render a new output format.
*pageOutput *pageOutput
@ -125,6 +129,10 @@ type pageState struct {
*pageCommon *pageCommon
} }
func (p *pageState) reusePageOutputContent() bool {
return p.pageOutputTemplateVariationsState.Load() == 1
}
func (p *pageState) Err() error { func (p *pageState) Err() error {
return nil return nil
} }
@ -394,56 +402,6 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil 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 { func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
p.layoutDescriptorInit.Do(func() { p.layoutDescriptorInit.Do(func() {
var section string var section string
@ -867,7 +825,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
if isRenderingSite { if isRenderingSite {
cp := p.pageOutput.cp cp := p.pageOutput.cp
if cp == nil { if cp == nil && p.reusePageOutputContent() {
// Look for content to reuse. // Look for content to reuse.
for i := 0; i < len(p.pageOutputs); i++ { for i := 0; i < len(p.pageOutputs); i++ {
if i == idx { if i == idx {
@ -875,7 +833,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
} }
po := p.pageOutputs[i] po := p.pageOutputs[i]
if po.cp != nil && po.cp.reuse { if po.cp != nil {
cp = po.cp cp = po.cp
break break
} }

View file

@ -17,6 +17,8 @@ import (
"html/template" "html/template"
"strings" "strings"
"go.uber.org/atomic"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
@ -36,7 +38,8 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) {
s := metaProvider.s s := metaProvider.s
ps := &pageState{ ps := &pageState{
pageOutput: nopPageOutput, pageOutput: nopPageOutput,
pageOutputTemplateVariationsState: atomic.NewUint32(0),
pageCommon: &pageCommon{ pageCommon: &pageCommon{
FileProvider: metaProvider, FileProvider: metaProvider,
AuthorProvider: metaProvider, AuthorProvider: metaProvider,

View file

@ -32,6 +32,7 @@ import (
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/alecthomas/chroma/lexers"
"github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/lazy"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
@ -109,16 +110,8 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
return err return err
} }
enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants) if hasShortcodeVariants {
p.pageOutputTemplateVariationsState.Store(2)
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()
} }
cp.workContent = p.contentToRender(cp.contentPlaceholders) cp.workContent = p.contentToRender(cp.contentPlaceholders)
@ -199,19 +192,10 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
return nil return nil
} }
// Recursive loops can only happen in content files with template code (shortcodes etc.) // There may be recursive loops in shortcodes and render hooks.
// Avoid creating new goroutines if we don't have to. cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
needTimeout := p.shortcodeState.hasShortcodes() || cp.renderHooks != nil return nil, initContent()
})
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()
})
}
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) { cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
cp.plain = helpers.StripHTML(string(cp.content)) cp.plain = helpers.StripHTML(string(cp.content))
@ -229,18 +213,14 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
} }
type renderHooks struct { type renderHooks struct {
hooks hooks.Renderers getRenderer hooks.GetRendererFunc
init sync.Once init sync.Once
} }
// pageContentOutput represents the Page content for a given output format. // pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct { type pageContentOutput struct {
f output.Format f output.Format
// If we can reuse this for other output formats.
reuse bool
reuseInit sync.Once
p *pageState p *pageState
// Lazy load dependencies // Lazy load dependencies
@ -250,13 +230,9 @@ type pageContentOutput struct {
placeholdersEnabled bool placeholdersEnabled bool
placeholdersEnabledInit sync.Once placeholdersEnabledInit sync.Once
// Renders Markdown hooks.
renderHooks *renderHooks 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 workContent []byte
dependencyTracker identity.Manager // Set in server mode. dependencyTracker identity.Manager // Set in server mode.
@ -440,55 +416,107 @@ func (p *pageContentOutput) initRenderHooks() error {
return nil return nil
} }
var initErr error
p.renderHooks.init.Do(func() { p.renderHooks.init.Do(func() {
ps := p.p if p.p.pageOutputTemplateVariationsState.Load() == 0 {
p.p.pageOutputTemplateVariationsState.Store(1)
c := ps.getContentConverter()
if c == nil || !c.Supports(converter.FeatureRenderHooks) {
return
} }
h, err := ps.createRenderHooks(p.f) type cacheKey struct {
if err != nil { tp hooks.RendererType
initErr = err id interface{}
return f output.Format
} }
p.renderHooks.hooks = h
if !p.renderHooksHaveVariants || h.IsZero() { renderCache := make(map[cacheKey]interface{})
// Check if there is a different render hooks template var renderCacheMu sync.Mutex
// for any of the other page output formats.
// If not, we can reuse this. p.renderHooks.getRenderer = func(tp hooks.RendererType, id interface{}) interface{} {
for _, po := range ps.pageOutputs { renderCacheMu.Lock()
if po.f.Name != p.f.Name { defer renderCacheMu.Unlock()
h2, err := ps.createRenderHooks(po.f)
if err != nil { key := cacheKey{tp: tp, id: id, f: p.f}
initErr = err if r, ok := renderCache[key]; ok {
return return r
}
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
} }
if h2.IsZero() {
continue
}
if p.renderHooks.hooks.IsZero() {
p.renderHooks.hooks = h2
}
p.renderHooksHaveVariants = !h2.Eq(p.renderHooks.hooks)
if p.renderHooksHaveVariants {
break
}
} }
} }
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.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 { 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) { func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
if err := cp.initRenderHooks(); err != nil {
return nil, err
}
c := cp.p.getContentConverter() c := cp.p.getContentConverter()
return cp.renderContentWithConverter(c, content, renderTOC) return cp.renderContentWithConverter(c, content, renderTOC)
} }
@ -521,7 +552,7 @@ func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, c
converter.RenderContext{ converter.RenderContext{
Src: content, Src: content,
RenderTOC: renderTOC, RenderTOC: renderTOC,
RenderHooks: cp.renderHooks.hooks, GetRenderer: cp.renderHooks.getRenderer,
}) })
if err == nil { 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. // these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface { type pagePerOutputProviders interface {
targetPather targetPather

View file

@ -428,8 +428,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
assertFunc(t, e.ext, s.RegularPages()) assertFunc(t, e.ext, s.RegularPages())
home, err := s.Info.Home() home := s.Info.Home()
b.Assert(err, qt.IsNil)
b.Assert(home, qt.Not(qt.IsNil)) b.Assert(home, qt.Not(qt.IsNil))
b.Assert(home.File().Path(), qt.Equals, homePath) b.Assert(home.File().Path(), qt.Equals, homePath)
b.Assert(content(home), qt.Contains, "Home Page Content") 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) 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, qt.Not(qt.IsNil))
c.Assert(home.TranslationKey(), qt.Equals, "home") c.Assert(home.TranslationKey(), qt.Equals, "home")
c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1") c.Assert(s.RegularPages()[0].TranslationKey(), qt.Equals, "page/k1")

View file

@ -150,7 +150,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
c.Assert(leafBundle1.Section(), qt.Equals, "b") c.Assert(leafBundle1.Section(), qt.Equals, "b")
sectionB := s.getPage(page.KindSection, "b") sectionB := s.getPage(page.KindSection, "b")
c.Assert(sectionB, qt.Not(qt.IsNil)) c.Assert(sectionB, qt.Not(qt.IsNil))
home, _ := s.Info.Home() home := s.Info.Home()
c.Assert(home.BundleType(), qt.Equals, files.ContentClassBranch) c.Assert(home.BundleType(), qt.Equals, files.ContentClassBranch)
// This is a root bundle and should live in the "home section" // 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.RegularPages()), qt.Equals, 8)
c.Assert(len(s.Pages()), qt.Equals, 16) c.Assert(len(s.Pages()), qt.Equals, 16)
//dumpPages(s.AllPages()...) // dumpPages(s.AllPages()...)
c.Assert(len(s.AllPages()), qt.Equals, 31) c.Assert(len(s.AllPages()), qt.Equals, 31)

View file

@ -30,6 +30,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/modules"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
@ -54,12 +55,11 @@ import (
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/pkg/errors"
_errors "github.com/pkg/errors" _errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
@ -1773,19 +1773,23 @@ var infoOnMissingLayout = map[string]bool{
"404": true, "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. // where ITEM is the thing being hooked.
type hookRenderer struct { type hookRendererTemplate struct {
templateHandler tpl.TemplateHandler templateHandler tpl.TemplateHandler
identity.SearchProvider identity.SearchProvider
templ tpl.Template 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) return hr.templateHandler.Execute(hr.templ, w, ctx)
} }
func (hr hookRenderer) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error { func (hr hookRendererTemplate) RenderHeading(w io.Writer, ctx hooks.HeadingContext) error {
return hr.templateHandler.Execute(hr.templ, w, ctx)
}
func (hr hookRendererTemplate) RenderCodeblock(w hugio.FlexiWriter, ctx hooks.CodeblockContext) error {
return hr.templateHandler.Execute(hr.templ, w, ctx) return hr.templateHandler.Execute(hr.templ, w, ctx)
} }

View file

@ -19,14 +19,10 @@ import (
// Sections returns the top level sections. // Sections returns the top level sections.
func (s *SiteInfo) Sections() page.Pages { func (s *SiteInfo) Sections() page.Pages {
home, err := s.Home() return s.Home().Sections()
if err == nil {
return home.Sections()
}
return nil
} }
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home". // Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
func (s *SiteInfo) Home() (page.Page, error) { func (s *SiteInfo) Home() page.Page {
return s.s.home, nil return s.s.home
} }

View file

@ -21,6 +21,7 @@ import (
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter/hooks" "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/markup_config"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -34,7 +35,7 @@ type ProviderConfig struct {
ContentFs afero.Fs ContentFs afero.Fs
Logger loggers.Logger Logger loggers.Logger
Exec *hexec.Exec Exec *hexec.Exec
Highlight func(code, lang, optsStr string) (string, error) highlight.Highlighter
} }
// ProviderProvider creates converter providers. // ProviderProvider creates converter providers.
@ -127,9 +128,10 @@ type DocumentContext struct {
// RenderContext holds contextual information about the content to render. // RenderContext holds contextual information about the content to render.
type RenderContext struct { type RenderContext struct {
Src []byte Src []byte
RenderTOC bool RenderTOC bool
RenderHooks hooks.Renderers
GetRenderer hooks.GetRendererFunc
} }
var FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks") var FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")

View file

@ -14,15 +14,17 @@
package hooks package hooks
import ( import (
"fmt"
"io" "io"
"strings"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal/attributes"
) )
var _ AttributesOptionsSliceProvider = (*attributes.AttributesHolder)(nil)
type AttributesProvider interface { type AttributesProvider interface {
Attributes() map[string]string Attributes() map[string]interface{}
} }
type LinkContext interface { type LinkContext interface {
@ -33,11 +35,30 @@ type LinkContext interface {
PlainText() string 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 { type LinkRenderer interface {
RenderLink(w io.Writer, ctx LinkContext) error RenderLink(w io.Writer, ctx LinkContext) error
identity.Provider identity.Provider
} }
type CodeBlockRenderer interface {
RenderCodeblock(w hugio.FlexiWriter, ctx CodeblockContext) error
identity.Provider
}
// HeadingContext contains accessors to all attributes that a HeadingRenderer // HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading. // can use to render a heading.
type HeadingContext interface { type HeadingContext interface {
@ -63,70 +84,13 @@ type HeadingRenderer interface {
identity.Provider identity.Provider
} }
type Renderers struct { type RendererType int
LinkRenderer LinkRenderer
ImageRenderer LinkRenderer
HeadingRenderer HeadingRenderer
}
func (r Renderers) Eq(other interface{}) bool { const (
ro, ok := other.(Renderers) LinkRendererType RendererType = iota + 1
if !ok { ImageRendererType
return false HeadingRendererType
} CodeBlockRendererType
)
if r.IsZero() || ro.IsZero() { type GetRendererFunc func(t RendererType, id interface{}) interface{}
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
}
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()))
}
return sb.String()
}

View file

@ -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>",
)
}

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -17,12 +17,12 @@ package goldmark
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"math/bits"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes" "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" "github.com/gohugoio/hugo/identity"
@ -32,16 +32,13 @@ import (
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
hl "github.com/yuin/goldmark-highlighting"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text" "github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
) )
// Provider is the package entry point. // Provider is the package entry point.
@ -104,7 +101,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
) )
if mcfg.Highlight.CodeFences { if mcfg.Highlight.CodeFences {
extensions = append(extensions, newHighlighting(mcfg.Highlight)) extensions = append(extensions, codeblocks.New())
} }
if cfg.Extensions.Table { if cfg.Extensions.Table {
@ -178,65 +175,6 @@ func (c converterResult) GetIdentities() identity.Identities {
return c.ids 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"} var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) { 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 result = buf
pctx := c.newParserContext(ctx) pctx := c.newParserContext(ctx)
reader := text.NewReader(ctx.Src) reader := text.NewReader(ctx.Src)
@ -261,15 +199,15 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
parser.WithContext(pctx), parser.WithContext(pctx),
) )
rcx := &renderContextDataHolder{ rcx := &render.RenderContextDataHolder{
rctx: ctx, Rctx: ctx,
dctx: c.ctx, Dctx: c.ctx,
ids: identity.NewManager(converterIdentity), IDs: identity.NewManager(converterIdentity),
} }
w := &renderContext{ w := &render.Context{
bufWriter: buf, BufWriter: buf,
renderContextData: rcx, ContextData: rcx,
} }
if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil { 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{ return converterResult{
Result: buf, Result: buf,
ids: rcx.ids.GetIdentities(), ids: rcx.IDs.GetIdentities(),
toc: pctx.TableOfContents(), toc: pctx.TableOfContents(),
}, nil }, nil
} }
@ -309,63 +247,3 @@ func (p *parserContext) TableOfContents() tableofcontents.Root {
} }
return 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>")
}

View file

@ -20,6 +20,7 @@ import (
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config" "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/highlight" "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) 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"}) conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
c.Assert(err, qt.IsNil) 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) c.Assert(err, qt.IsNil)
return b 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```" content := "```" + language + "\n" + code + "\n```"
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{}) conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil) 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) c.Assert(err, qt.IsNil)
return string(b.Bytes()) return string(b.Bytes())
@ -391,7 +410,7 @@ LINE5
// TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func. // 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>") 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") 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) { c.Run("Highlight lines, default config", func(c *qt.C) {

View file

@ -36,12 +36,12 @@ func TestAttributeExclusion(t *testing.T) {
--- ---
title: "p1" title: "p1"
--- ---
## Heading {class="a" onclick="alert('heading')" linenos="inline"} ## Heading {class="a" onclick="alert('heading')"}
> Blockquote > 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 foo
~~~ ~~~
-- layouts/_default/single.html -- -- 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 // Issue 9504
func TestLinkInTitle(t *testing.T) { func TestLinkInTitle(t *testing.T) {
t.Parallel() 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) { func BenchmarkRenderHooks(b *testing.B) {
files := ` files := `
-- config.toml -- -- config.toml --

View file

@ -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)
}

View file

@ -16,11 +16,10 @@ package goldmark
import ( import (
"bytes" "bytes"
"strings" "strings"
"sync"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks" "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"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
@ -44,28 +43,6 @@ func newLinks() goldmark.Extender {
return &links{} 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 { type linkContext struct {
page interface{} page interface{}
destination string destination string
@ -104,7 +81,7 @@ type headingContext struct {
anchor string anchor string
text string text string
plainText string plainText string
*attributesHolder *attributes.AttributesHolder
} }
func (ctx headingContext) Page() interface{} { func (ctx headingContext) Page() interface{} {
@ -143,52 +120,17 @@ func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer)
reg.Register(ast.KindHeading, r.renderHeading) 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) { func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Image) n := node.(*ast.Image)
var h hooks.Renderers var lr hooks.LinkRenderer
ctx, ok := w.(*renderContext) ctx, ok := w.(*render.Context)
if ok { if ok {
h = ctx.RenderContext().RenderHooks h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
ok = h.ImageRenderer != nil ok = h != nil
if ok {
lr = h.(hooks.LinkRenderer)
}
} }
if !ok { if !ok {
@ -197,15 +139,15 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
if entering { if entering {
// Store the current pos so we can capture the rendered text. // 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 return ast.WalkContinue, nil
} }
pos := ctx.popPos() pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:] text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos) ctx.Buffer.Truncate(pos)
err := h.ImageRenderer.RenderLink( err := lr.RenderLink(
w, w,
linkContext{ linkContext{
page: ctx.DocumentContext().Document, 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 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) { func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link) n := node.(*ast.Link)
var h hooks.Renderers var lr hooks.LinkRenderer
ctx, ok := w.(*renderContext) ctx, ok := w.(*render.Context)
if ok { if ok {
h = ctx.RenderContext().RenderHooks h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
ok = h.LinkRenderer != nil ok = h != nil
if ok {
lr = h.(hooks.LinkRenderer)
}
} }
if !ok { if !ok {
@ -264,15 +209,15 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
if entering { if entering {
// Store the current pos so we can capture the rendered text. // 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 return ast.WalkContinue, nil
} }
pos := ctx.popPos() pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:] text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos) ctx.Buffer.Truncate(pos)
err := h.LinkRenderer.RenderLink( err := lr.RenderLink(
w, w,
linkContext{ linkContext{
page: ctx.DocumentContext().Document, 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, // 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, // 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. // to make sure we search the entire chain on changes.
ctx.AddIdentity(h.LinkRenderer) ctx.AddIdentity(lr)
return ast.WalkContinue, err return ast.WalkContinue, err
} }
@ -319,12 +264,15 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
} }
n := node.(*ast.AutoLink) n := node.(*ast.AutoLink)
var h hooks.Renderers var lr hooks.LinkRenderer
ctx, ok := w.(*renderContext) ctx, ok := w.(*render.Context)
if ok { if ok {
h = ctx.RenderContext().RenderHooks h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
ok = h.LinkRenderer != nil ok = h != nil
if ok {
lr = h.(hooks.LinkRenderer)
}
} }
if !ok { if !ok {
@ -337,7 +285,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
url = "mailto:" + url url = "mailto:" + url
} }
err := h.LinkRenderer.RenderLink( err := lr.RenderLink(
w, w,
linkContext{ linkContext{
page: ctx.DocumentContext().Document, 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, // 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, // 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. // to make sure we search the entire chain on changes.
ctx.AddIdentity(h.LinkRenderer) ctx.AddIdentity(lr)
return ast.WalkContinue, err 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) { func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading) n := node.(*ast.Heading)
var h hooks.Renderers var hr hooks.HeadingRenderer
ctx, ok := w.(*renderContext) ctx, ok := w.(*render.Context)
if ok { if ok {
h = ctx.RenderContext().RenderHooks h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
ok = h.HeadingRenderer != nil ok = h != nil
if ok {
hr = h.(hooks.HeadingRenderer)
}
} }
if !ok { if !ok {
@ -397,11 +348,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
if entering { if entering {
// Store the current pos so we can capture the rendered text. // 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 return ast.WalkContinue, nil
} }
pos := ctx.popPos() pos := ctx.PopPos()
text := ctx.Buffer.Bytes()[pos:] text := ctx.Buffer.Bytes()[pos:]
ctx.Buffer.Truncate(pos) ctx.Buffer.Truncate(pos)
// All ast.Heading nodes are guaranteed to have an attribute called "id" // 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") anchori, _ := n.AttributeString("id")
anchor := anchori.([]byte) anchor := anchori.([]byte)
err := h.HeadingRenderer.RenderHeading( err := hr.RenderHeading(
w, w,
headingContext{ headingContext{
page: ctx.DocumentContext().Document, page: ctx.DocumentContext().Document,
@ -417,11 +368,11 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
anchor: string(anchor), anchor: string(anchor),
text: string(text), text: string(text),
plainText: string(n.Text(source)), 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 return ast.WalkContinue, err
} }
@ -432,7 +383,7 @@ func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, n
_, _ = w.WriteString("<h") _, _ = w.WriteString("<h")
_ = w.WriteByte("0123456"[n.Level]) _ = w.WriteByte("0123456"[n.Level])
if n.Attributes() != nil { if n.Attributes() != nil {
r.renderAttributesForNode(w, node) attributes.RenderASTAttributes(w, node.Attributes()...)
} }
_ = w.WriteByte('>') _ = w.WriteByte('>')
} else { } else {

View file

@ -18,6 +18,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
@ -27,6 +28,8 @@ import (
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
var nopGetRenderer = func(t hooks.RendererType, id interface{}) interface{} { return nil }
func TestToc(t *testing.T) { func TestToc(t *testing.T) {
c := qt.New(t) c := qt.New(t)
@ -58,7 +61,7 @@ And then some.
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{}) conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil) 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) c.Assert(err, qt.IsNil)
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3, false) got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents"> c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@ -108,7 +111,7 @@ func TestEscapeToc(t *testing.T) {
"# `echo codeblock`", "# `echo codeblock`",
}, "\n") }, "\n")
// content := "" // 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) c.Assert(err, qt.IsNil)
got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false) got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents"> c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@ -120,7 +123,7 @@ func TestEscapeToc(t *testing.T) {
</ul> </ul>
</nav>`, qt.Commentf(got)) </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) c.Assert(err, qt.IsNil)
got = b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false) got = b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(1, 2, false)
c.Assert(got, qt.Equals, `<nav id="TableOfContents"> c.Assert(got, qt.Equals, `<nav id="TableOfContents">

View file

@ -20,6 +20,7 @@ import (
"strings" "strings"
"github.com/alecthomas/chroma/formatters/html" "github.com/alecthomas/chroma/formatters/html"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
@ -46,6 +47,9 @@ type Config struct {
// Use inline CSS styles. // Use inline CSS styles.
NoClasses bool NoClasses bool
// No highlighting.
NoHl bool
// When set, line numbers will be printed. // When set, line numbers will be printed.
LineNos bool LineNos bool
LineNumbersInTable bool LineNumbersInTable bool
@ -60,6 +64,9 @@ type Config struct {
// A space separated list of line numbers, e.g. “3-8 10-20”. // A space separated list of line numbers, e.g. “3-8 10-20”.
Hl_Lines string 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 sets the number of characters for a tab. Defaults to 4.
TabWidth int TabWidth int
@ -80,9 +87,19 @@ func (cfg Config) ToHTMLOptions() []html.Option {
html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors), html.LinkableLineNumbers(cfg.AnchorLineNos, lineAnchors),
} }
if cfg.Hl_Lines != "" { if cfg.Hl_Lines != "" || cfg.HL_lines_parsed != nil {
ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines) var ranges [][2]int
if err == nil { 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)) options = append(options, html.HighlightLines(ranges))
} }
} }
@ -90,14 +107,32 @@ func (cfg Config) ToHTMLOptions() []html.Option {
return options 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 { func applyOptionsFromString(opts string, cfg *Config) error {
optsm, err := parseOptions(opts) optsm, err := parseHightlightOptions(opts)
if err != nil { if err != nil {
return err return err
} }
return mapstructure.WeakDecode(optsm, cfg) 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 // ApplyLegacyConfig applies legacy config from back when we had
// Pygments. // Pygments.
func ApplyLegacyConfig(cfg config.Provider, conf *Config) error { func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
@ -128,7 +163,7 @@ func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
return nil return nil
} }
func parseOptions(in string) (map[string]interface{}, error) { func parseHightlightOptions(in string) (map[string]interface{}, error) {
in = strings.Trim(in, " ") in = strings.Trim(in, " ")
opts := make(map[string]interface{}) opts := make(map[string]interface{})
@ -142,19 +177,57 @@ func parseOptions(in string) (map[string]interface{}, error) {
if len(keyVal) != 2 { if len(keyVal) != 2 {
return opts, fmt.Errorf("invalid Highlight option: %s", key) return opts, fmt.Errorf("invalid Highlight option: %s", key)
} }
if key == "linenos" { opts[key] = keyVal[1]
opts[key] = keyVal[1] != "false"
if keyVal[1] == "table" || keyVal[1] == "inline" {
opts["lineNumbersInTable"] = keyVal[1] == "table"
}
} else {
opts[key] = keyVal[1]
}
} }
normalizeHighlightOptions(opts)
return opts, nil 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 // startLine compensates for https://github.com/alecthomas/chroma/issues/30
func hlLinesToRanges(startLine int, s string) ([][2]int, error) { func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
var ranges [][2]int var ranges [][2]int

View file

@ -16,47 +16,155 @@ package highlight
import ( import (
"fmt" "fmt"
gohtml "html" gohtml "html"
"html/template"
"io" "io"
"strconv"
"strings" "strings"
"github.com/alecthomas/chroma" "github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html" "github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers" "github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles" "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 { func New(cfg Config) Highlighter {
return Highlighter{ return chromaHighlighter{
cfg: cfg, 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 cfg Config
} }
func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) { func (h chromaHighlighter) Highlight(code, lang string, opts interface{}) (string, error) {
if optsStr == "" {
return highlight(code, lang, h.cfg)
}
cfg := h.cfg cfg := h.cfg
if err := applyOptionsFromString(optsStr, &cfg); err != nil { if err := applyOptions(opts, &cfg); err != nil {
return "", err
}
var b strings.Builder
if err := highlight(&b, code, lang, nil, cfg); err != nil {
return "", err return "", err
} }
return highlight(code, lang, cfg) return b.String(), nil
} }
func highlight(code, lang string, cfg Config) (string, error) { func (h chromaHighlighter) HighlightCodeBlock(ctx hooks.CodeblockContext, opts interface{}) (HightlightResult, error) {
w := &strings.Builder{} 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 var lexer chroma.Lexer
if lang != "" { if lang != "" {
lexer = lexers.Get(lang) lexer = lexers.Get(lang)
} }
if lexer == nil && cfg.GuessSyntax { if lexer == nil && (cfg.GuessSyntax && !cfg.NoHl) {
lexer = lexers.Analyse(code) lexer = lexers.Analyse(code)
if lexer == nil { if lexer == nil {
lexer = lexers.Fallback 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, wrapper.Start(true, ""))
fmt.Fprint(w, gohtml.EscapeString(code)) fmt.Fprint(w, gohtml.EscapeString(code))
fmt.Fprint(w, wrapper.End(true)) fmt.Fprint(w, wrapper.End(true))
return w.String(), nil return nil
} }
style := styles.Get(cfg.Style) style := styles.Get(cfg.Style)
@ -80,7 +188,7 @@ func highlight(code, lang string, cfg Config) (string, error) {
iterator, err := lexer.Tokenise(nil, code) iterator, err := lexer.Tokenise(nil, code)
if err != nil { if err != nil {
return "", err return err
} }
options := cfg.ToHTMLOptions() options := cfg.ToHTMLOptions()
@ -88,25 +196,13 @@ func highlight(code, lang string, cfg Config) (string, error) {
formatter := html.New(options...) formatter := html.New(options...)
fmt.Fprint(w, `<div class="highlight">`) writeDivStart(w, attributes)
if err := formatter.Format(w, style, iterator); err != nil { if err := formatter.Format(w, style, iterator); err != nil {
return "", err return err
} }
fmt.Fprint(w, `</div>`) writeDivEnd(w)
return w.String(), nil return nil
}
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),
}
}
} }
func getPreWrapper(language string) preWrapper { func getPreWrapper(language string) preWrapper {
@ -150,3 +246,25 @@ func (p preWrapper) End(code bool) string {
func WritePreEnd(w io.Writer) { func WritePreEnd(w io.Writer) {
fmt.Fprint(w, preEnd) 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>")
}

View file

@ -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('"')
}
}

View file

@ -39,11 +39,8 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
return nil, err return nil, err
} }
if cfg.Highlight == nil { if cfg.Highlighter == nil {
h := highlight.New(markupConfig.Highlight) cfg.Highlighter = highlight.New(markupConfig.Highlight)
cfg.Highlight = func(code, lang, optsStr string) (string, error) {
return h.Highlight(code, lang, optsStr)
}
} }
cfg.MarkupConfig = markupConfig cfg.MarkupConfig = markupConfig
@ -95,7 +92,7 @@ type ConverterProvider interface {
Get(name string) converter.Provider Get(name string) converter.Provider
// Default() converter.Provider // Default() converter.Provider
GetMarkupConfig() markup_config.Config GetMarkupConfig() markup_config.Config
Highlight(code, lang, optsStr string) (string, error) GetHighlighter() highlight.Highlighter
} }
type converterRegistry struct { type converterRegistry struct {
@ -112,8 +109,8 @@ func (r *converterRegistry) Get(name string) converter.Provider {
return r.converters[strings.ToLower(name)] return r.converters[strings.ToLower(name)]
} }
func (r *converterRegistry) Highlight(code, lang, optsStr string) (string, error) { func (r *converterRegistry) GetHighlighter() highlight.Highlighter {
return r.config.Highlight(code, lang, optsStr) return r.config.Highlighter
} }
func (r *converterRegistry) GetMarkupConfig() markup_config.Config { func (r *converterRegistry) GetMarkupConfig() markup_config.Config {

View file

@ -27,8 +27,7 @@ import (
// Provider is the package entry point. // Provider is the package entry point.
var Provider converter.ProviderProvider = provide{} var Provider converter.ProviderProvider = provide{}
type provide struct { type provide struct{}
}
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) { func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
return converter.NewProvider("org", func(ctx converter.DocumentContext) (converter.Converter, error) { return converter.NewProvider("org", func(ctx converter.DocumentContext) (converter.Converter, error) {

View file

@ -31,9 +31,15 @@ var reservedSections = map[string]bool{
type LayoutDescriptor struct { type LayoutDescriptor struct {
Type string Type string
Section string Section string
Kind string
Lang string // E.g. "page", but also used for the _markup render kinds, e.g. "render-image".
Layout string 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 indicates what we should only look for the above layout.
LayoutOverride bool LayoutOverride bool
@ -139,6 +145,12 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
} }
if d.RenderingHook { 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.addLayoutVariations(d.Kind)
b.addSectionType() b.addSectionType()
} }

View file

@ -32,6 +32,7 @@ type Site interface {
Language() *langs.Language Language() *langs.Language
RegularPages() Pages RegularPages() Pages
Pages() Pages Pages() Pages
Home() Page
IsServer() bool IsServer() bool
ServerPort() int ServerPort() int
Title() string Title() string
@ -89,6 +90,10 @@ func (t testSite) Language() *langs.Language {
return t.l return t.l
} }
func (t testSite) Home() Page {
return nil
}
func (t testSite) Pages() Pages { func (t testSite) Pages() Pages {
return nil return nil
} }

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -40,14 +40,14 @@ func init() {
ns.AddMethodMapping(ctx.Eq, ns.AddMethodMapping(ctx.Eq,
[]string{"eq"}, []string{"eq"},
[][2]string{ [][2]string{
{`{{ if eq .Section "blog" }}current{{ end }}`, `current`}, {`{{ if eq .Section "blog" }}current-section{{ end }}`, `current-section`},
}, },
) )
ns.AddMethodMapping(ctx.Ge, ns.AddMethodMapping(ctx.Ge,
[]string{"ge"}, []string{"ge"},
[][2]string{ [][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!`},
}, },
) )

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

73
tpl/diagrams/diagrams.go Normal file
View file

@ -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),
}
}

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Hugo Authors. All rights reserved. // Copyright 2022 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -11,32 +11,28 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package os package diagrams
import ( import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/htesting/hqt"
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
) )
func TestInit(t *testing.T) { const name = "diagrams"
c := qt.New(t)
var found bool
var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { func init() {
ns = nsf(&deps.Deps{}) f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
if ns.Name == name { ctx := &Diagrams{
found = true d: d,
break
} }
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(args ...interface{}) (interface{}, error) { return ctx, nil },
}
return ns
} }
c.Assert(found, qt.Equals, true) internal.AddTemplateFuncsNamespace(f)
ctx, err := ns.Context()
c.Assert(err, qt.IsNil)
c.Assert(ctx, hqt.IsSameType, &Namespace{})
} }

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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())
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -19,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
_os "os" _os "os"
"path/filepath"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -27,17 +28,9 @@ import (
// New returns a new instance of the os-namespaced template functions. // New returns a new instance of the os-namespaced template functions.
func New(d *deps.Deps) *Namespace { 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{ 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, deps: d,
} }
} }
@ -45,6 +38,7 @@ func New(d *deps.Deps) *Namespace {
// Namespace provides template functions for the "os" namespace. // Namespace provides template functions for the "os" namespace.
type Namespace struct { type Namespace struct {
readFileFs afero.Fs readFileFs afero.Fs
workFs afero.Fs
deps *deps.Deps 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 // readFile reads the file named by filename in the given filesystem
// and returns the contents as a string. // and returns the contents as a string.
func readFile(fs afero.Fs, filename string) (string, error) { func readFile(fs afero.Fs, filename string) (string, error) {
if filename == "" { filename = filepath.Clean(filename)
return "", errors.New("readFile needs a filename") if filename == "" || filename == "." || filename == string(_os.PathSeparator) {
return "", errors.New("invalid filename")
} }
b, err := afero.ReadFile(fs, filename) b, err := afero.ReadFile(fs, filename)
@ -101,7 +96,7 @@ func (ns *Namespace) ReadDir(i interface{}) ([]_os.FileInfo, error) {
return nil, err return nil, err
} }
list, err := afero.ReadDir(ns.deps.Fs.WorkingDir, path) list, err := afero.ReadDir(ns.workFs, path)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read directory %q: %s", path, err) return nil, fmt.Errorf("failed to read directory %q: %s", path, err)
} }

View file

@ -11,34 +11,26 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package os package os_test
import ( import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/tpl/os"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
) )
func TestReadFile(t *testing.T) { func TestReadFile(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t)
workingDir := "/home/hugo" b := newFileTestBuilder(t).Build()
v := config.New() // helpers.PrintFs(b.H.PathSpec.BaseFs.Work, "", _os.Stdout)
v.Set("workingDir", workingDir)
// f := newTestFuncsterWithViper(v) ns := os.New(b.H.Deps)
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)
for _, test := range []struct { for _, test := range []struct {
filename string filename string
@ -53,13 +45,13 @@ func TestReadFile(t *testing.T) {
result, err := ns.ReadFile(test.filename) result, err := ns.ReadFile(test.filename)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
@ -67,15 +59,8 @@ func TestFileExists(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) c := qt.New(t)
workingDir := "/home/hugo" b := newFileTestBuilder(t).Build()
ns := os.New(b.H.Deps)
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)
for _, test := range []struct { for _, test := range []struct {
filename string filename string
@ -101,15 +86,8 @@ func TestFileExists(t *testing.T) {
func TestStat(t *testing.T) { func TestStat(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := newFileTestBuilder(t).Build()
workingDir := "/home/hugo" ns := os.New(b.H.Deps)
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)
for _, test := range []struct { for _, test := range []struct {
filename string filename string
@ -123,11 +101,28 @@ func TestStat(t *testing.T) {
result, err := ns.Stat(test.filename) result, err := ns.Stat(test.filename)
if test.expect == nil { if test.expect == nil {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result.Size(), qt.Equals, test.expect) 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",
},
)
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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)
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -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{})
}

View file

@ -281,15 +281,10 @@ func (t *templateExec) UnusedTemplates() []tpl.FileInfo {
for _, ts := range t.main.templates { for _, ts := range t.main.templates {
ti := ts.info ti := ts.info
if strings.HasPrefix(ti.name, "_internal/") { if strings.HasPrefix(ti.name, "_internal/") || ti.realFilename == "" {
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.
continue continue
} }
if _, found := t.templateUsageTracker[ti.name]; !found { if _, found := t.templateUsageTracker[ti.name]; !found {
unused = append(unused, ti) unused = append(unused, ti)
} }
@ -740,6 +735,7 @@ func (t *templateHandler) extractIdentifiers(line string) []string {
} }
//go:embed embedded/templates/* //go:embed embedded/templates/*
//go:embed embedded/templates/_default/*
var embededTemplatesFs embed.FS var embededTemplatesFs embed.FS
func (t *templateHandler) loadEmbedded() error { func (t *templateHandler) loadEmbedded() error {
@ -757,9 +753,19 @@ func (t *templateHandler) loadEmbedded() error {
// to write the templates to Go files. // to write the templates to Go files.
templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n"))) templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/") name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
templateName := name
if err := t.AddTemplate(internalPathPrefix+name, templ); err != nil { // For the render hooks it does not make sense to preseve the
return err // 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 { if aliases, found := embeddedTemplatesAliases[name]; found {

View file

@ -38,6 +38,7 @@ import (
_ "github.com/gohugoio/hugo/tpl/crypto" _ "github.com/gohugoio/hugo/tpl/crypto"
_ "github.com/gohugoio/hugo/tpl/data" _ "github.com/gohugoio/hugo/tpl/data"
_ "github.com/gohugoio/hugo/tpl/debug" _ "github.com/gohugoio/hugo/tpl/debug"
_ "github.com/gohugoio/hugo/tpl/diagrams"
_ "github.com/gohugoio/hugo/tpl/encoding" _ "github.com/gohugoio/hugo/tpl/encoding"
_ "github.com/gohugoio/hugo/tpl/fmt" _ "github.com/gohugoio/hugo/tpl/fmt"
_ "github.com/gohugoio/hugo/tpl/hugo" _ "github.com/gohugoio/hugo/tpl/hugo"

View file

@ -11,223 +11,74 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package tplimpl package tplimpl_test
import ( import (
"bytes"
"context"
"fmt" "fmt"
"path/filepath" "strings"
"reflect"
"testing" "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/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) { func TestTemplateFuncsExamples(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t)
workingDir := "/home/hugo" 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**"
---
`
v := newTestConfig() b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
).Build()
v.Set("workingDir", workingDir) d := b.H.Sites[0].Deps
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) var (
templates []string
afero.WriteFile(fs.Source, filepath.Join(workingDir, "files", "README.txt"), []byte("Hugo Rocks!"), 0755) expected []string
)
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()}
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns := nsf(d) ns := nsf(d)
for _, mm := range ns.MethodMappings { for _, mm := range ns.MethodMappings {
for i, example := range mm.Examples { for _, example := range mm.Examples {
in, expected := example[0], example[1] if strings.Contains(example[0], "errorf") {
d.WithTemplate = func(templ tpl.TemplateManager) error { // This will fail the build, so skip for now.
c.Assert(templ.AddTemplate("test", in), qt.IsNil) continue
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)
} }
templates = append(templates, example[0])
expected = append(expected, example[1])
} }
} }
} }
}
files += fmt.Sprintf("-- layouts/_default/single.html --\n%s\n", strings.Join(templates, "\n"))
// TODO(bep) it would be dandy to put this one into the partials package, but b = hugolib.NewIntegrationTestBuilder(
// we have some package cycle issues to solve first. hugolib.IntegrationTestConfig{
func TestPartialCached(t *testing.T) { T: t,
t.Parallel() TxtarString: files,
NeedsOsFS: true,
c := qt.New(t) },
).Build()
partial := `Now: {{ now.UnixNano }}`
name := "testing" b.AssertFileContent("public/blog/hugo-rocks/index.html", expected...)
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)
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)
}
}
})
} }

View file

@ -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
}

View file

@ -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{})
}

View file

@ -11,13 +11,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package transform package transform_test
import ( import (
"testing" "testing"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/tpl/transform"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
) )
@ -25,13 +26,14 @@ import (
func TestRemarshal(t *testing.T) { func TestRemarshal(t *testing.T) {
t.Parallel() t.Parallel()
v := config.New() b := hugolib.NewIntegrationTestBuilder(
v.Set("contentDir", "content") hugolib.IntegrationTestConfig{T: t},
ns := New(newDeps(v)) ).Build()
ns := transform.New(b.H.Deps)
c := qt.New(t) c := qt.New(t)
c.Run("Roundtrip variants", func(c *qt.C) { c.Run("Roundtrip variants", func(c *qt.C) {
tomlExample := `title = 'Test Metadata' tomlExample := `title = 'Test Metadata'
[[resources]] [[resources]]
@ -129,7 +131,6 @@ title: Test Metadata
} }
} }
}) })
c.Run("Comments", func(c *qt.C) { c.Run("Comments", func(c *qt.C) {

View file

@ -19,6 +19,9 @@ import (
"html/template" "html/template"
"github.com/gohugoio/hugo/cache/namedmemcache" "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/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
@ -65,18 +68,28 @@ func (ns *Namespace) Highlight(s interface{}, lang string, opts ...interface{})
return "", err return "", err
} }
sopts := "" var optsv interface{}
if len(opts) > 0 { if len(opts) > 0 {
sopts, err = cast.ToStringE(opts[0]) optsv = opts[0]
if err != nil {
return "", err
}
} }
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 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. // HTMLEscape returns a copy of s with reserved HTML characters escaped.
func (ns *Namespace) HTMLEscape(s interface{}) (string, error) { func (ns *Namespace) HTMLEscape(s interface{}) (string, error) {
ss, err := cast.ToStringE(s) 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. // Markdownify renders a given input from Markdown to HTML.
func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) { func (ns *Namespace) Markdownify(s interface{}) (template.HTML, error) {
defer herrors.Recover()
ss, err := cast.ToStringE(s) ss, err := cast.ToStringE(s)
if err != nil { if err != nil {
return "", err return "", err
} }
b, err := ns.deps.ContentSpec.RenderMarkdown([]byte(ss)) home := ns.deps.Site.Home()
if err != nil { if home == nil {
return "", err panic("home must not be nil")
} }
sss, err := home.RenderString(ss)
// Strip if this is a short inline type of text. // 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. // 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 return helpers.StripHTML(ss), nil
} }
func (ns *Namespace) Reset() {
ns.cache.Clear()
}

View file

@ -11,13 +11,15 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package transform package transform_test
import ( import (
"html/template" "html/template"
"testing" "testing"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/tpl/transform"
"github.com/spf13/afero" "github.com/spf13/afero"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -32,10 +34,11 @@ type tstNoStringer struct{}
func TestEmojify(t *testing.T) { func TestEmojify(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -49,23 +52,23 @@ func TestEmojify(t *testing.T) {
result, err := ns.Emojify(test.s) result, err := ns.Emojify(test.s)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
func TestHighlight(t *testing.T) { func TestHighlight(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
v.Set("contentDir", "content")
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -82,23 +85,23 @@ func TestHighlight(t *testing.T) {
result, err := ns.Highlight(test.s, test.lang, test.opts) result, err := ns.Highlight(test.s, test.lang, test.opts)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(string(result), qt.Contains, test.expect.(string)) b.Assert(string(result), qt.Contains, test.expect.(string))
} }
} }
func TestHTMLEscape(t *testing.T) { func TestHTMLEscape(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
v.Set("contentDir", "content")
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -112,23 +115,23 @@ func TestHTMLEscape(t *testing.T) {
result, err := ns.HTMLEscape(test.s) result, err := ns.HTMLEscape(test.s)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
func TestHTMLUnescape(t *testing.T) { func TestHTMLUnescape(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
v.Set("contentDir", "content")
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -142,23 +145,23 @@ func TestHTMLUnescape(t *testing.T) {
result, err := ns.HTMLUnescape(test.s) result, err := ns.HTMLUnescape(test.s)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
func TestMarkdownify(t *testing.T) { func TestMarkdownify(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
v.Set("contentDir", "content")
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -171,23 +174,24 @@ func TestMarkdownify(t *testing.T) {
result, err := ns.Markdownify(test.s) result, err := ns.Markdownify(test.s)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
// Issue #3040 // Issue #3040
func TestMarkdownifyBlocksOfText(t *testing.T) { func TestMarkdownifyBlocksOfText(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
v := config.New() hugolib.IntegrationTestConfig{T: t},
v.Set("contentDir", "content") ).Build()
ns := New(newDeps(v))
ns := transform.New(b.H.Deps)
text := ` text := `
#First #First
@ -202,17 +206,18 @@ And then some.
` `
result, err := ns.Markdownify(text) result, err := ns.Markdownify(text)
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, template.HTML( 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")) "<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) { func TestPlainify(t *testing.T) {
t.Parallel() t.Parallel()
c := qt.New(t) b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t},
).Build()
v := config.New() ns := transform.New(b.H.Deps)
ns := New(newDeps(v))
for _, test := range []struct { for _, test := range []struct {
s interface{} s interface{}
@ -225,13 +230,13 @@ func TestPlainify(t *testing.T) {
result, err := ns.Plainify(test.s) result, err := ns.Plainify(test.s)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
continue continue
} }
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }

View file

@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package transform package transform_test
import ( import (
"fmt" "fmt"
@ -19,7 +19,8 @@ import (
"strings" "strings"
"testing" "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/common/hugio"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
@ -80,12 +81,14 @@ func (t testContentResource) Key() string {
} }
func TestUnmarshal(t *testing.T) { func TestUnmarshal(t *testing.T) {
v := config.New() b := hugolib.NewIntegrationTestBuilder(
ns := New(newDeps(v)) hugolib.IntegrationTestConfig{T: t},
c := qt.New(t) ).Build()
ns := transform.New(b.H.Deps)
assertSlogan := func(m map[string]interface{}) { 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 { 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 {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) { 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] first := r[0]
c.Assert(len(first), qt.Equals, 5) b.Assert(len(first), qt.Equals, 5)
c.Assert(first[1], qt.Equals, "Ford") b.Assert(first[1], qt.Equals, "Ford")
}}, }},
{testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]interface{}{"delimiter": ";"}, func(r [][]string) { {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) { {"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) { {"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: ` {testContentResource{key: "r1", content: `
% This is a comment % This is a comment
a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment": "%"}, func(r [][]string) { 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 // errors
{"thisisnotavaliddataformat", nil, false}, {"thisisnotavaliddataformat", nil, false},
@ -144,7 +147,7 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
{tstNoStringer{}, nil, false}, {tstNoStringer{}, nil, false},
} { } {
ns.cache.Clear() ns.Reset()
var args []interface{} var args []interface{}
@ -156,29 +159,32 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment"
result, err := ns.Unmarshal(args...) result, err := ns.Unmarshal(args...)
if b, ok := test.expect.(bool); ok && !b { if bb, ok := test.expect.(bool); ok && !bb {
c.Assert(err, qt.Not(qt.IsNil)) b.Assert(err, qt.Not(qt.IsNil))
} else if fn, ok := test.expect.(func(m map[string]interface{})); ok { } 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{}) m, ok := result.(map[string]interface{})
c.Assert(ok, qt.Equals, true) b.Assert(ok, qt.Equals, true)
fn(m) fn(m)
} else if fn, ok := test.expect.(func(r [][]string)); ok { } else if fn, ok := test.expect.(func(r [][]string)); ok {
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
r, ok := result.([][]string) r, ok := result.([][]string)
c.Assert(ok, qt.Equals, true) b.Assert(ok, qt.Equals, true)
fn(r) fn(r)
} else { } else {
c.Assert(err, qt.IsNil) b.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, test.expect) b.Assert(result, qt.Equals, test.expect)
} }
} }
} }
func BenchmarkUnmarshalString(b *testing.B) { func BenchmarkUnmarshalString(b *testing.B) {
v := config.New() bb := hugolib.NewIntegrationTestBuilder(
ns := New(newDeps(v)) hugolib.IntegrationTestConfig{T: b},
).Build()
ns := transform.New(bb.H.Deps)
const numJsons = 100 const numJsons = 100
@ -200,8 +206,11 @@ func BenchmarkUnmarshalString(b *testing.B) {
} }
func BenchmarkUnmarshalResource(b *testing.B) { func BenchmarkUnmarshalResource(b *testing.B) {
v := config.New() bb := hugolib.NewIntegrationTestBuilder(
ns := New(newDeps(v)) hugolib.IntegrationTestConfig{T: b},
).Build()
ns := transform.New(bb.H.Deps)
const numJsons = 100 const numJsons = 100

View file

@ -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{})
}