From 08fdca9d9365eaf1e496a12e2af5e18617bd0e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 17 Feb 2022 13:04:00 +0100 Subject: 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 --- common/hugio/writers.go | 8 + .../gohugoioTheme/layouts/_default/baseof.html | 8 + docs/content/en/content-management/diagrams.md | 217 ++++++++++++++++++ .../_default/_markup/render-codeblock-goat.html | 18 ++ .../_default/_markup/render-codeblock-mermaid.html | 4 + go.mod | 5 +- go.sum | 8 + helpers/content.go | 12 +- hugolib/content_render_hooks_test.go | 4 +- hugolib/integrationtest_builder.go | 4 +- hugolib/language_content_dir_test.go | 2 +- hugolib/page.go | 64 +----- hugolib/page__new.go | 5 +- hugolib/page__per_output.go | 173 ++++++++------- hugolib/page_test.go | 5 +- hugolib/pagebundler_test.go | 4 +- hugolib/site.go | 16 +- hugolib/site_sections.go | 10 +- markup/converter/converter.go | 10 +- markup/converter/hooks/hooks.go | 100 +++------ markup/goldmark/codeblocks/integration_test.go | 115 ++++++++++ markup/goldmark/codeblocks/render.go | 159 +++++++++++++ markup/goldmark/codeblocks/transform.go | 53 +++++ markup/goldmark/convert.go | 146 +----------- markup/goldmark/convert_test.go | 25 ++- markup/goldmark/integration_test.go | 141 +++++++++++- markup/goldmark/internal/render/context.go | 81 +++++++ markup/goldmark/render_hooks.go | 143 ++++-------- markup/goldmark/toc_test.go | 9 +- markup/highlight/config.go | 99 +++++++-- markup/highlight/highlight.go | 178 ++++++++++++--- markup/internal/attributes/attributes.go | 219 ++++++++++++++++++ markup/markup.go | 13 +- markup/org/convert.go | 3 +- output/layout.go | 18 +- resources/page/site.go | 5 + tpl/cast/init_test.go | 43 ---- tpl/collections/init_test.go | 43 ---- tpl/compare/init.go | 4 +- tpl/compare/init_test.go | 42 ---- tpl/crypto/init_test.go | 42 ---- tpl/data/init_test.go | 47 ---- tpl/debug/init_test.go | 44 ---- tpl/diagrams/diagrams.go | 73 ++++++ tpl/diagrams/init.go | 38 ++++ tpl/encoding/init_test.go | 42 ---- tpl/fmt/init_test.go | 44 ---- tpl/hugo/init_test.go | 49 ----- tpl/images/init_test.go | 42 ---- tpl/inflect/init_test.go | 43 ---- tpl/lang/init_test.go | 48 ---- tpl/math/init_test.go | 42 ---- tpl/os/init_test.go | 42 ---- tpl/os/os.go | 21 +- tpl/os/os_test.go | 73 +++--- tpl/partials/init_test.go | 46 ---- tpl/path/init_test.go | 43 ---- tpl/reflect/init_test.go | 43 ---- tpl/safe/init_test.go | 43 ---- tpl/site/init_test.go | 49 ----- tpl/strings/init_test.go | 45 ---- tpl/templates/init_test.go | 42 ---- tpl/time/init_test.go | 48 ---- .../_default/_markup/render-codeblock-goat.html | 1 + tpl/tplimpl/template.go | 24 +- tpl/tplimpl/template_funcs.go | 1 + tpl/tplimpl/template_funcs_test.go | 245 ++++----------------- tpl/tplimpl/template_info_test.go | 58 ----- tpl/transform/init_test.go | 42 ---- tpl/transform/remarshal_test.go | 15 +- tpl/transform/transform.go | 41 +++- tpl/transform/transform_test.go | 111 +++++----- tpl/transform/unmarshal_test.go | 61 ++--- tpl/urls/init_test.go | 45 ---- 74 files changed, 1905 insertions(+), 2004 deletions(-) create mode 100644 docs/content/en/content-management/diagrams.md create mode 100644 docs/layouts/_default/_markup/render-codeblock-goat.html create mode 100644 docs/layouts/_default/_markup/render-codeblock-mermaid.html create mode 100644 markup/goldmark/codeblocks/integration_test.go create mode 100644 markup/goldmark/codeblocks/render.go create mode 100644 markup/goldmark/codeblocks/transform.go create mode 100644 markup/goldmark/internal/render/context.go create mode 100644 markup/internal/attributes/attributes.go delete mode 100644 tpl/cast/init_test.go delete mode 100644 tpl/collections/init_test.go delete mode 100644 tpl/compare/init_test.go delete mode 100644 tpl/crypto/init_test.go delete mode 100644 tpl/data/init_test.go delete mode 100644 tpl/debug/init_test.go create mode 100644 tpl/diagrams/diagrams.go create mode 100644 tpl/diagrams/init.go delete mode 100644 tpl/encoding/init_test.go delete mode 100644 tpl/fmt/init_test.go delete mode 100644 tpl/hugo/init_test.go delete mode 100644 tpl/images/init_test.go delete mode 100644 tpl/inflect/init_test.go delete mode 100644 tpl/lang/init_test.go delete mode 100644 tpl/math/init_test.go delete mode 100644 tpl/os/init_test.go delete mode 100644 tpl/partials/init_test.go delete mode 100644 tpl/path/init_test.go delete mode 100644 tpl/reflect/init_test.go delete mode 100644 tpl/safe/init_test.go delete mode 100644 tpl/site/init_test.go delete mode 100644 tpl/strings/init_test.go delete mode 100644 tpl/templates/init_test.go delete mode 100644 tpl/time/init_test.go create mode 100644 tpl/tplimpl/embedded/templates/_default/_markup/render-codeblock-goat.html delete mode 100644 tpl/tplimpl/template_info_test.go delete mode 100644 tpl/transform/init_test.go delete mode 100644 tpl/urls/init_test.go 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" }} + + +{{ end }} + + 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
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 "" }} +
+ {{ with diagrams.Goat .Code }} + + {{ .Body }} + + {{ end }} +
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 @@ +
+ {{- .Code | safeHTML }} +
+{{ .Page.Store.Set "hasMermaid" true }} diff --git a/go.mod b/go.mod index 8227c2c67..71bdd5607 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go v1.43.5 github.com/bep/debounce v1.2.0 github.com/bep/gitmap v1.1.2 + github.com/bep/goat v0.5.0 github.com/bep/godartsass v0.12.0 github.com/bep/golibsass v1.0.0 github.com/bep/gowebp v0.1.0 @@ -19,7 +20,7 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/evanw/esbuild v0.14.22 github.com/fortytw2/leaktest v1.3.0 - github.com/frankban/quicktest v1.14.0 + github.com/frankban/quicktest v1.14.2 github.com/fsnotify/fsnotify v1.5.1 github.com/getkin/kin-openapi v0.85.0 github.com/ghodss/yaml v1.0.0 @@ -57,7 +58,7 @@ require ( github.com/spf13/pflag v1.0.5 github.com/tdewolff/minify/v2 v2.9.29 github.com/yuin/goldmark v1.4.7 - github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 + go.uber.org/atomic v1.9.0 gocloud.dev v0.20.0 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd diff --git a/go.sum b/go.sum index 01e8ff60c..4ddc55855 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,10 @@ github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840= github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY= +github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e h1:On3hMv9ffG+0fgPIjKPXiFu5QVS9jM1Vzr5/ghmSLy4= +github.com/bep/goat v0.0.0-20220222160823-cc97a132eb5e/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= +github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= +github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= github.com/bep/godartsass v0.12.0 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E= github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4= github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw= @@ -239,6 +243,8 @@ github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns= +github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= @@ -623,6 +629,8 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8= diff --git a/helpers/content.go b/helpers/content.go index 157f75079..3e674bca4 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/afero" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup" @@ -47,8 +48,8 @@ var ( // ContentSpec provides functionality to render markdown content. type ContentSpec struct { Converters markup.ConverterProvider - MardownConverter converter.Converter // Markdown converter with no document context anchorNameSanitizer converter.AnchorNameSanitizer + getRenderer func(t hooks.RendererType, id interface{}) interface{} // SummaryLength is the length of the summary that Hugo extracts from a content. summaryLength int @@ -88,7 +89,6 @@ func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero. if err != nil { return nil, err } - spec.MardownConverter = conv if as, ok := conv.(converter.AnchorNameSanitizer); ok { spec.anchorNameSanitizer = as } else { @@ -192,14 +192,6 @@ func ExtractTOC(content []byte) (newcontent []byte, toc []byte) { return } -func (c *ContentSpec) RenderMarkdown(src []byte) ([]byte, error) { - b, err := c.MardownConverter.Convert(converter.RenderContext{Src: src}) - if err != nil { - return nil, err - } - return b.Bytes(), nil -} - func (c *ContentSpec) SanitizeAnchorName(s string) string { return c.anchorNameSanitizer.SanitizeAnchorName(s) } diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go index edfeaa82a..33ebe1f41 100644 --- a/hugolib/content_render_hooks_test.go +++ b/hugolib/content_render_hooks_test.go @@ -231,8 +231,8 @@ SHORT3| b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`) // We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`) b.AssertFileContent("public/blog/p4/index.html", `

IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END

`) - // The regular markdownify func currently gets regular links. - b.AssertFileContent("public/blog/p5/index.html", "Inner Link: Inner Link\n") + // 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 "" - } - - 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:Go Code\nGo Code: fmt.Println(\"Hello, World!\");\n|\nGo Language: go|", + "

Golang Code

\nGo Code: fmt.Println(\"Hello, Golang!\");\n|\nGo Language: golang|", + "

Bash Code

\n
32echo "l1";\n33",
+	)
+}
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(`
") -} - -func writeDivEnd(w util.BufWriter) { - w.WriteString("
") -} 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, "
echo "Hugo Rocks!"\n
") result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown") - c.Assert(result, qt.Equals, "
echo "Hugo Rocks!"\n
") + c.Assert(result, qt.Equals, "
echo "Hugo Rocks!"\n
") }) 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", + "
LINE1\n
", + "Code Fences No Lexer\n
LINE1\n
", + "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("') } 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, `