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