]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
markup/asciidocext: Improve Asciidoctor integration
authorJoe Mooring <joe.mooring@veriphor.com>
Fri, 24 Oct 2025 19:11:54 +0000 (12:11 -0700)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 24 Nov 2025 22:03:53 +0000 (23:03 +0100)
Fixes an issue where improper attribute derivation from the page's
relative permalink caused failures with `outdir`, `imagesoutdir`, and
`imagesdir` when `markup.asciidocext.workingFolderCurrent` is enabled.
The updated logic now correctly handles:

- Multi-byte characters
- Multilingual multi-host sites
- Site builds from a subdirectory
- Pages using ugly URLs

Supports diagram caching as implemented in v3.1.0 of the asciidoctor-diagram
extension:

- Enables caching by default
- Sets default cache location to the compiled value of caches.misc.dir

Reduces duration of integration tests by:

- Generating GoAT diagrams instead of Ditaa diagrams
- Taking advantage of asciidoctor-diagram caching

Closes #9202
Closes #10183
Closes #10473
Closes #14160

17 files changed:
.github/workflows/test.yml
cache/filecache/filecache_config.go
config/allconfig/configlanguage.go
config/configProvider.go
docs/data/docs.yaml
htesting/test_helpers.go
hugolib/page_test.go
hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
hugolib/rebuild_test.go
hugolib/securitypolicies_test.go
markup/asciidocext/asciidoc_integration_test.go [new file with mode: 0644]
markup/asciidocext/convert.go
markup/asciidocext/convert_test.go
markup/asciidocext/internal/converter.go
markup/markup_config/config.go
markup/markup_config/config_test.go
resources/page/page_markup_integration_test.go

index eb9db2eb83bfe48aed6808515a3eabe72f6deb57..b8670cb4d19ed8cd1a9fedf2f6bc38da25ced0b3 100644 (file)
@@ -46,19 +46,12 @@ jobs:
         uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
         with:
           ruby-version: "3.4.5"
-      - name: Install gems
+      - name: Install Ruby gems
         run: |
           gem install asciidoctor -v "2.0.26"
           gem install asciidoctor-diagram -v "3.1.0"
-          gem install asciidoctor-diagram-ditaamini -v "1.0.3"
       - name: Install GoAT
         run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
-      - name: Install Java # required by asciidoctor-diagram-ditaamini
-        uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
-        with:
-          distribution: temurin
-          java-version: "25"
-          java-package: jre
       - name: Install Python
         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
         with:
index a71ddb474c4b053e92528054ef44635e53e13450..d5298117470721b13e73e9ca9701f5f269c53827 100644 (file)
@@ -51,11 +51,18 @@ const (
 
 type Configs map[string]FileCacheConfig
 
+// CacheDirModules returns the compiled path to the modules cache.
 // For internal use.
 func (c Configs) CacheDirModules() string {
        return c[CacheKeyModules].DirCompiled
 }
 
+// CacheDirMisc returns the compiled path to the misc cache.
+// For internal use.
+func (c Configs) CacheDirMisc() string {
+       return c[CacheKeyMisc].DirCompiled
+}
+
 var defaultCacheConfigs = Configs{
        CacheKeyModules: {
                MaxAge: -1,
index 9a7789efdd7c704c3760197744a98e01f4a32cf9..d27b1ba4f97f0e7e0ee74d04de98a79b87f69a26 100644 (file)
@@ -135,6 +135,10 @@ func (c ConfigLanguage) WorkingDir() string {
        return c.m.Base.WorkingDir
 }
 
+func (c ConfigLanguage) CacheDirMisc() string {
+       return c.config.Caches.CacheDirMisc()
+}
+
 func (c ConfigLanguage) Quiet() bool {
        return c.m.Base.Internal.Quiet
 }
index 34e7ea3653d614d5e58e1e96e749ae713bb06fa0..4e6878c1f8ed0ab0051edc79f85ff0443d147015 100644 (file)
@@ -79,6 +79,7 @@ type AllProvider interface {
        WorkingDir() string
        EnableEmoji() bool
        ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions
+       CacheDirMisc() string
 }
 
 // We cannot import the media package as that would create a circular dependency.
index 9cd80c4f7afe6d9f1ee570fc799e42dfadd86343..40b701d9ba165b5aabb6172b6c3b25008b0dfeb8 100644 (file)
@@ -121,8 +121,8 @@ chroma:
     Name: C
   - Aliases:
     - csharp
-    - c#
-    Name: C#
+    - 'c#'
+    Name: 'C#'
   - Aliases:
     - cpp
     - c++
@@ -522,7 +522,7 @@ chroma:
     Name: Modula-2
   - Aliases:
     - mojo
-    - "\U0001F525"
+    - 🔥
     Name: Mojo
   - Aliases:
     - monkeyc
@@ -1077,10 +1077,12 @@ config:
         - '**'
       high: 0s
       low: 0s
+    respectCacheControlNoStoreInRequest: true
+    respectCacheControlNoStoreInResponse: false
   archeTypeDir: archetypes
   assetDir: assets
-  author: null
-  baseURL: ""
+  author: {}
+  baseURL: ''
   build:
     buildStats:
       disableClasses: false
@@ -1088,14 +1090,14 @@ config:
       disableTags: false
       enable: false
     cacheBusters:
-    - source: (postcss|tailwind)\.config\.js
+    - source: '(postcss|tailwind)\.config\.js'
       target: (css|styles|scss|sass)
     noJSConfigInAssets: false
     useResourceCacheWhen: fallback
   buildDrafts: false
   buildExpired: false
   buildFuture: false
-  cacheDir: ""
+  cacheDir: ''
   caches:
     assets:
       dir: :resourceDir/_gen
@@ -1120,7 +1122,7 @@ config:
       maxAge: -1
   canonifyURLs: false
   capitalizeListTitles: true
-  cascade: []
+  cascade: null
   cleanDestinationDir: false
   contentDir: content
   contentTypes:
@@ -1130,10 +1132,14 @@ config:
     text/org: {}
     text/pandoc: {}
     text/rst: {}
-  copyright: ""
+  copyright: ''
   dataDir: data
   defaultContentLanguage: en
   defaultContentLanguageInSubdir: false
+  defaultContentRole: guest
+  defaultContentRoleInSubdir: false
+  defaultContentVersion: ''
+  defaultContentVersionInSubdir: false
   defaultOutputFormat: html
   deployment:
     confirm: false
@@ -1143,7 +1149,7 @@ config:
     matchers: null
     maxDeletes: 256
     order: null
-    target: ""
+    target: ''
     targets: null
     workers: 10
   disableAliases: false
@@ -1187,25 +1193,25 @@ config:
   ignoreCache: false
   ignoreFiles: null
   ignoreLogs: null
-  ignoreVendorPaths: ""
+  ignoreVendorPaths: ''
   imaging:
     bgColor: '#ffffff'
     hint: photo
     quality: 75
     resampleFilter: box
-  languageCode: ""
+  languageCode: ''
   languages:
     en:
       disabled: false
-      languageCode: ""
-      languageDirection: ""
-      languageName: ""
-      title: ""
+      languageCode: ''
+      languageDirection: ''
+      languageName: ''
+      title: ''
       weight: 0
   layoutDir: layouts
   mainSections: null
   markup:
-    asciidocExt:
+    asciiDocExt:
       attributes: {}
       backend: html5
       extensions: []
@@ -1287,9 +1293,9 @@ config:
       anchorLineNos: false
       codeFences: true
       guessSyntax: false
-      hl_Lines: ""
+      hl_Lines: ''
       hl_inline: false
-      lineAnchors: ""
+      lineAnchors: ''
       lineNoStart: 1
       lineNos: false
       lineNumbersInTable: true
@@ -1506,8 +1512,8 @@ config:
         keepSpecialComments: true
         keepWhitespace: false
         templateDelims:
-        - ""
-        - ""
+        - ''
+        - ''
       js:
         keepVarNames: false
         precision: 0
@@ -1522,93 +1528,142 @@ config:
       xml:
         keepWhitespace: false
   module:
-    auth: ""
+    auth: ''
     hugoVersion:
       extended: false
-      max: ""
-      min: ""
+      max: ''
+      min: ''
     imports: null
     mounts:
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: content
       target: content
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: data
       target: data
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: layouts
       target: layouts
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: i18n
       target: i18n
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: archetypes
       target: archetypes
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: assets
       target: assets
     - disableWatch: false
-      excludeFiles: null
-      includeFiles: null
-      lang: ""
+      files: null
+      sites:
+        complements:
+          languages: null
+          roles: null
+          versions: null
+        matrix:
+          languages: null
+          roles: null
+          versions: null
       source: static
       target: static
     noProxy: none
-    noVendor: ""
+    noVendor: ''
     params: null
     private: '*.*'
     proxy: direct
     replacements: null
     vendorClosest: false
-    workspace: "off"
-  newContentEditor: ""
+    workspace: 'off'
+  newContentEditor: ''
   noBuildLock: false
   noChmod: false
   noTimes: false
   outputFormats:
-    "404":
-      baseName: ""
+    '404':
+      baseName: ''
       isHTML: true
       isPlainText: false
       mediaType: text/html
       noUgly: false
       notAlternative: true
-      path: ""
+      path: ''
       permalinkable: true
-      protocol: ""
-      rel: ""
+      protocol: ''
+      rel: ''
       root: false
       ugly: true
       weight: 0
     alias:
-      baseName: ""
+      baseName: ''
       isHTML: true
       isPlainText: false
       mediaType: text/html
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
-      rel: ""
+      protocol: ''
+      rel: ''
       root: false
       ugly: true
       weight: 0
@@ -1621,7 +1676,7 @@ config:
       notAlternative: false
       path: amp
       permalinkable: true
-      protocol: ""
+      protocol: ''
       rel: amphtml
       root: false
       ugly: false
@@ -1633,7 +1688,7 @@ config:
       mediaType: text/calendar
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
       protocol: webcal://
       rel: alternate
@@ -1647,9 +1702,9 @@ config:
       mediaType: text/css
       noUgly: false
       notAlternative: true
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: stylesheet
       root: false
       ugly: false
@@ -1661,24 +1716,24 @@ config:
       mediaType: text/csv
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: alternate
       root: false
       ugly: false
       weight: 0
     gotmpl:
-      baseName: ""
+      baseName: ''
       isHTML: false
       isPlainText: true
       mediaType: text/x-gotmpl
       noUgly: false
       notAlternative: true
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
-      rel: ""
+      protocol: ''
+      rel: ''
       root: false
       ugly: false
       weight: 0
@@ -1689,9 +1744,9 @@ config:
       mediaType: text/html
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: true
-      protocol: ""
+      protocol: ''
       rel: canonical
       root: false
       ugly: false
@@ -1703,9 +1758,9 @@ config:
       mediaType: application/json
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: alternate
       root: false
       ugly: false
@@ -1717,9 +1772,9 @@ config:
       mediaType: text/markdown
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: alternate
       root: false
       ugly: false
@@ -1731,9 +1786,9 @@ config:
       mediaType: text/plain
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: alternate
       root: true
       ugly: false
@@ -1745,9 +1800,9 @@ config:
       mediaType: application/rss+xml
       noUgly: true
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: alternate
       root: false
       ugly: false
@@ -1759,9 +1814,9 @@ config:
       mediaType: application/xml
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: sitemap
       root: false
       ugly: true
@@ -1773,9 +1828,9 @@ config:
       mediaType: application/xml
       noUgly: false
       notAlternative: false
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: sitemap
       root: true
       ugly: true
@@ -1787,9 +1842,9 @@ config:
       mediaType: application/manifest+json
       noUgly: false
       notAlternative: true
-      path: ""
+      path: ''
       permalinkable: false
-      protocol: ""
+      protocol: ''
       rel: manifest
       root: false
       ugly: false
@@ -1815,7 +1870,7 @@ config:
     nextPrevInSectionSortOrder: desc
     nextPrevSortOrder: desc
   paginate: 0
-  paginatePath: ""
+  paginatePath: ''
   pagination:
     disableAliases: false
     pagerSize: 10
@@ -1840,10 +1895,6 @@ config:
     instagram:
       disable: false
       simple: false
-    twitter:
-      disable: false
-      enableDNT: false
-      simple: false
     vimeo:
       disable: false
       enableDNT: false
@@ -1856,29 +1907,29 @@ config:
       disable: false
       privacyEnhanced: false
   publishDir: public
-  refLinksErrorLevel: ""
-  refLinksNotFoundURL: ""
+  refLinksErrorLevel: ''
+  refLinksNotFoundURL: ''
   related:
     includeNewer: false
     indices:
     - applyFilter: false
       cardinalityThreshold: 0
       name: keywords
-      pattern: ""
+      pattern: ''
       toLower: false
       type: basic
       weight: 100
     - applyFilter: false
       cardinalityThreshold: 0
       name: date
-      pattern: ""
+      pattern: ''
       toLower: false
       type: basic
       weight: 10
     - applyFilter: false
       cardinalityThreshold: 0
       name: tags
-      pattern: ""
+      pattern: ''
       toLower: false
       type: basic
       weight: 80
@@ -1888,7 +1939,10 @@ config:
   removePathAccents: false
   renderSegments: null
   resourceDir: resources
-  sectionPagesMenu: ""
+  roles:
+    guest:
+      weight: 0
+  sectionPagesMenu: ''
   security:
     enableInlineShortcodes: false
     exec:
@@ -1900,7 +1954,7 @@ config:
       - ^postcss$
       - ^tailwindcss$
       osEnv:
-      - (?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$
+      - '(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$'
     funcs:
       getenv:
       - ^HUGO_
@@ -1911,32 +1965,27 @@ config:
       - (?i)GET|POST
       urls:
       - .*
-  segments: null
+  segments: {}
   server:
     headers: null
     redirects:
     - force: false
       from: /**
       fromHeaders: null
-      fromRe: ""
+      fromRe: ''
       status: 404
       to: /404.html
   services:
     disqus:
-      shortname: ""
+      shortname: ''
     googleAnalytics:
-      id: ""
-    instagram:
-      accessToken: ""
-      disableInlineCSS: false
+      id: ''
     rss:
       limit: -1
-    twitter:
-      disableInlineCSS: false
     x:
       disableInlineCSS: false
   sitemap:
-    changeFreq: ""
+    changeFreq: ''
     disable: false
     filename: sitemap.xml
     priority: -1
@@ -1945,6 +1994,7 @@ config:
   - static
   staticDir0: null
   staticDir1: null
+  staticDir10: null
   staticDir2: null
   staticDir3: null
   staticDir4: null
@@ -1953,7 +2003,6 @@ config:
   staticDir7: null
   staticDir8: null
   staticDir9: null
-  staticDir10: null
   summaryLength: 70
   taxonomies:
     category: categories
@@ -1962,12 +2011,15 @@ config:
   templateMetricsHints: false
   theme: null
   themesDir: themes
-  timeZone: ""
+  timeZone: ''
   timeout: 60s
-  title: ""
+  title: ''
   titleCaseStyle: AP
-  uglyURLs: null
-  workingDir: ""
+  uglyURLs: {}
+  versions:
+    v1.0.0:
+      weight: 0
+  workingDir: ''
 config_helpers:
   mergeStrategy:
     build:
@@ -2020,6 +2072,8 @@ config_helpers:
       _merge: none
     related:
       _merge: none
+    roles:
+      _merge: none
     security:
       _merge: none
     segments:
@@ -2032,6 +2086,8 @@ config_helpers:
       _merge: none
     taxonomies:
       _merge: none
+    versions:
+      _merge: none
 output:
   layouts: {}
 tpl:
@@ -2069,7 +2125,7 @@ tpl:
         Aliases:
         - after
         Args:
-        - "n"
+        - 'n'
         - l
         Description: After returns all the items after the first n items in list l.
         Examples: []
@@ -2078,11 +2134,16 @@ tpl:
         - append
         Args:
         - args
-        Description: "Append appends args up to the last one to the slice in the last
-          argument.\nThis construct allows template constructs like this:\n\n\t{{
-          $pages = $pages | append $p2 $p1 }}\n\nNote that with 2 arguments where
-          both are slices of the same type,\nthe first slice will be appended to the
-          second:\n\n\t{{ $pages = $pages | append .Site.RegularPages }}"
+        Description: |-
+          Append appends args up to the last one to the slice in the last argument.
+          This construct allows template constructs like this:
+          
+               {{ $pages = $pages | append $p2 $p1 }}
+          
+          Note that with 2 arguments where both are slices of the same type,
+          the first slice will be appended to the second:
+          
+               {{ $pages = $pages | append .Site.RegularPages }}
         Examples: []
       Apply:
         Aliases:
@@ -2092,27 +2153,29 @@ tpl:
         - c
         - fname
         - args
-        Description: Apply takes an array or slice c and returns a new slice with
-          the function fname applied over it.
+        Description: Apply takes an array or slice c and returns a new slice with the function fname applied over it.
         Examples: []
       Complement:
         Aliases:
         - complement
         Args:
         - ls
-        Description: "Complement gives the elements in the last element of ls that
-          are not in\nany of the others.\n\nAll elements of ls must be slices or arrays
-          of comparable types.\n\nThe reasoning behind this rather clumsy API is so
-          we can do this in the templates:\n\n\t{{ $c := .Pages | complement $last4
-          }}"
+        Description: |-
+          Complement gives the elements in the last element of ls that are not in
+          any of the others.
+          
+          All elements of ls must be slices or arrays of comparable types.
+          
+          The reasoning behind this rather clumsy API is so we can do this in the templates:
+          
+               {{ $c := .Pages | complement $last4 }}
         Examples:
-        - - '{{ slice "a" "b" "c" "d" "e" "f" | complement (slice "b" "c") (slice
-            "d" "e") }}'
+        - - '{{ slice "a" "b" "c" "d" "e" "f" | complement (slice "b" "c") (slice "d" "e") }}'
           - '[a f]'
       D:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Delimit:
         Aliases:
@@ -2162,11 +2225,9 @@ tpl:
         Args:
         - l
         - v
-        Description: In returns whether v is in the list l.  l may be an array or
-          slice.
+        Description: In returns whether v is in the list l.  l may be an array or slice.
         Examples:
-        - - '{{ if in "this string contains a substring" "substring" }}Substring found!{{
-            end }}'
+        - - '{{ if in "this string contains a substring" "substring" }}Substring found!{{ end }}'
           - Substring found!
       Index:
         Aliases:
@@ -2178,9 +2239,9 @@ tpl:
           Index returns the result of indexing its first argument by the following
           arguments. Thus "index x 1 2 3" is, in Go syntax, x[1][2][3]. Each
           indexed item must be a map, slice, or array.
-
+          
           Adapted from Go stdlib src/text/template/funcs.go.
-
+          
           We deviate from the stdlib mostly because of https://github.com/golang/go/issues/14751.
         Examples: []
       Intersect:
@@ -2230,17 +2291,14 @@ tpl:
         Description: |-
           Merge creates a copy of the final parameter in params and merges the preceding
           parameters into it in reverse order.
-
+          
           Currently only maps are supported. Key handling is case insensitive.
         Examples:
-        - - '{{ dict "title" "Hugo Rocks!" | collections.Merge (dict "title" "Default
-            Title" "description" "Yes, Hugo Rocks!") | sort }}'
+        - - '{{ dict "title" "Hugo Rocks!" | collections.Merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!") | sort }}'
           - '[Yes, Hugo Rocks! Hugo Rocks!]'
-        - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!")
-            (dict "title" "Hugo Rocks!") | sort }}'
+        - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!") (dict "title" "Hugo Rocks!") | sort }}'
           - '[Yes, Hugo Rocks! Hugo Rocks!]'
-        - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!")
-            (dict "title" "Hugo Rocks!") (dict "extra" "For reals!") | sort }}'
+        - - '{{ merge (dict "title" "Default Title" "description" "Yes, Hugo Rocks!") (dict "title" "Hugo Rocks!") (dict "extra" "For reals!") | sort }}'
           - '[Yes, Hugo Rocks! For reals! Hugo Rocks!]'
       NewScratch:
         Aliases:
@@ -2250,9 +2308,8 @@ tpl:
           NewScratch creates a new Scratch which can be used to store values in a
           thread safe way.
         Examples:
-        - - '{{ $scratch := newScratch }}{{ $scratch.Add "b" 2 }}{{ $scratch.Add "b"
-            2 }}{{ $scratch.Get "b" }}'
-          - "4"
+        - - '{{ $scratch := newScratch }}{{ $scratch.Add "b" 2 }}{{ $scratch.Add "b" 2 }}{{ $scratch.Get "b" }}'
+          - '4'
       Querify:
         Aliases:
         - querify
@@ -2262,27 +2319,32 @@ tpl:
           Querify returns a URL query string composed of the given key-value pairs,
           encoded and sorted by key.
         Examples:
-        - - '{{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those")
-            | safeHTML }}'
+        - - '{{ (querify "foo" 1 "bar" 2 "baz" "with spaces" "qux" "this&that=those") | safeHTML }}'
           - bar=2&baz=with+spaces&foo=1&qux=this%26that%3Dthose
-        - - <a href="https://www.google.com?{{ (querify "q" "test" "page" 3) | safeURL
-            }}">Search</a>
+        - - <a href="https://www.google.com?{{ (querify "q" "test" "page" 3) | safeURL }}">Search</a>
           - <a href="https://www.google.com?page=3&amp;q=test">Search</a>
         - - '{{ slice "foo" 1 "bar" 2 | querify | safeHTML }}'
           - bar=2&foo=1
       Reverse:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Seq:
         Aliases:
         - seq
         Args:
         - args
-        Description: "Seq creates a sequence of integers from args. It's named and
-          used as GNU's seq.\n\nExamples:\n\n\t3 => 1, 2, 3\n\t1 2 4 => 1, 3\n\t-3
-          => -1, -2, -3\n\t1 4 => 1, 2, 3, 4\n\t1 -2 => 1, 0, -1, -2"
+        Description: |-
+          Seq creates a sequence of integers from args. It's named and used as GNU's seq.
+          
+          Examples:
+          
+               3 => 1, 2, 3
+               1 2 4 => 1, 3
+               -3 => -1, -2, -3
+               1 4 => 1, 2, 3, 4
+               1 -2 => 1, 0, -1, -2
         Examples:
         - - '{{ seq 3 }}'
           - '[1 2 3]'
@@ -2342,8 +2404,7 @@ tpl:
         - uniq
         Args:
         - l
-        Description: Uniq returns a new list with duplicate elements in the list l
-          removed.
+        Description: Uniq returns a new list with duplicate elements in the list l removed.
         Examples:
         - - '{{ slice 1 2 3 2 | uniq }}'
           - '[1 2 3]'
@@ -2367,7 +2428,7 @@ tpl:
         - v2
         Description: |-
           Conditional can be used as a ternary operator.
-
+          
           It returns v1 if cond is true, else v2.
         Examples:
         - - '{{ cond (eq (add 2 2) 4) "2+2 is 4" "what?" | safeHTML }}'
@@ -2394,8 +2455,7 @@ tpl:
         Args:
         - first
         - others
-        Description: Eq returns the boolean truth of arg1 == arg2 || arg1 == arg3
-          || arg1 == arg4.
+        Description: Eq returns the boolean truth of arg1 == arg2 || arg1 == arg3 || arg1 == arg4.
         Examples:
         - - '{{ if eq .Section "blog" }}current-section{{ end }}'
           - current-section
@@ -2405,8 +2465,7 @@ tpl:
         Args:
         - first
         - others
-        Description: Ge returns the boolean truth of arg1 >= arg2 && arg1 >= arg3
-          && arg1 >= arg4.
+        Description: Ge returns the boolean truth of arg1 >= arg2 && arg1 >= arg3 && arg1 >= arg4.
         Examples:
         - - '{{ if ge hugo.Version "0.80" }}Reasonable new Hugo version!{{ end }}'
           - Reasonable new Hugo version!
@@ -2416,8 +2475,7 @@ tpl:
         Args:
         - first
         - others
-        Description: Gt returns the boolean truth of arg1 > arg2 && arg1 > arg3 &&
-          arg1 > arg4.
+        Description: Gt returns the boolean truth of arg1 > arg2 && arg1 > arg3 && arg1 > arg4.
         Examples: []
       Le:
         Aliases:
@@ -2425,8 +2483,7 @@ tpl:
         Args:
         - first
         - others
-        Description: Le returns the boolean truth of arg1 <= arg2 && arg1 <= arg3
-          && arg1 <= arg4.
+        Description: Le returns the boolean truth of arg1 <= arg2 && arg1 <= arg3 && arg1 <= arg4.
         Examples: []
       Lt:
         Aliases:
@@ -2434,13 +2491,12 @@ tpl:
         Args:
         - first
         - others
-        Description: Lt returns the boolean truth of arg1 < arg2 && arg1 < arg3 &&
-          arg1 < arg4.
+        Description: Lt returns the boolean truth of arg1 < arg2 && arg1 < arg3 && arg1 < arg4.
         Examples: []
       LtCollate:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Ne:
         Aliases:
@@ -2448,14 +2504,13 @@ tpl:
         Args:
         - first
         - others
-        Description: Ne returns the boolean truth of arg1 != arg2 && arg1 != arg3
-          && arg1 != arg4.
+        Description: Ne returns the boolean truth of arg1 != arg2 && arg1 != arg3 && arg1 != arg4.
         Examples: []
     crypto:
       FNV32a:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       HMAC:
         Aliases:
@@ -2509,7 +2564,7 @@ tpl:
       Quoted:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Sass:
         Aliases:
@@ -2521,12 +2576,12 @@ tpl:
       TailwindCSS:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Unquoted:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     data:
       GetCSV:
@@ -2561,51 +2616,45 @@ tpl:
           Dump returns a object dump of val as a string.
           Note that not every value passed to Dump will print so nicely, but
           we'll improve on that.
-
+          
           We recommend using the "go" Chroma lexer to format the output
           nicely.
-
+          
           Also note that the output from Dump may change from Hugo version to the next,
           so don't depend on a specific output.
         Examples:
-        - - |-
-            {{ $m := newScratch }}
-            {{ $m.Set "Hugo" "Rocks!" }}
-            {{ $m.Values | debug.Dump | safeHTML }}
-          - |-
-            {
-              "Hugo": "Rocks!"
-            }
+        - - '{{ $m := newScratch }}\n{{ $m.Set "Hugo" "Rocks!" }}\n{{ $m.Values | debug.Dump | safeHTML }}'
+          - '{\n  "Hugo": "Rocks!"\n}'
       TestDeprecationErr:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       TestDeprecationInfo:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       TestDeprecationWarn:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Timer:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       VisualizeSpaces:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     diagrams:
       Goat:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     encoding:
       Base64Decode:
@@ -2618,7 +2667,7 @@ tpl:
         - - '{{ "SGVsbG8gd29ybGQ=" | base64Decode }}'
           - Hello world
         - - '{{ 42 | base64Encode | base64Decode }}'
-          - "42"
+          - '42'
       Base64Encode:
         Aliases:
         - base64Encode
@@ -2643,12 +2692,7 @@ tpl:
         - - '{{ (slice "A" "B" "C") | jsonify }}'
           - '["A","B","C"]'
         - - '{{ (slice "A" "B" "C") | jsonify (dict "indent" "  ") }}'
-          - |-
-            [
-              "A",
-              "B",
-              "C"
-            ]
+          - '[\n  "A",\n  "B",\n  "C"\n]'
     fmt:
       Errorf:
         Aliases:
@@ -2661,7 +2705,7 @@ tpl:
           It returns an empty string.
         Examples:
         - - '{{ errorf "%s." "failed" }}'
-          - ""
+          - ''
       Erroridf:
         Aliases:
         - erroridf
@@ -2675,11 +2719,11 @@ tpl:
           It returns an empty string.
         Examples:
         - - '{{ erroridf "my-err-id" "%s." "failed" }}'
-          - ""
+          - ''
       Errormf:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Print:
         Aliases:
@@ -2696,8 +2740,7 @@ tpl:
         Args:
         - format
         - args
-        Description: Printf returns string representation of args formatted with the
-          layout in format.
+        Description: Printf returns string representation of args formatted with the layout in format.
         Examples:
         - - '{{ printf "%s!" "works" }}'
           - works!
@@ -2706,8 +2749,7 @@ tpl:
         - println
         Args:
         - args
-        Description: Println returns string representation of args  ending with a
-          newline.
+        Description: Println returns string representation of args  ending with a newline.
         Examples:
         - - '{{ println "works!" }}'
           - |
@@ -2723,7 +2765,7 @@ tpl:
           It returns an empty string.
         Examples:
         - - '{{ warnf "%s." "warning" }}'
-          - ""
+          - ''
       Warnidf:
         Aliases:
         - warnidf
@@ -2737,11 +2779,11 @@ tpl:
           It returns an empty string.
         Examples:
         - - '{{ warnidf "my-warn-id" "%s." "warning" }}'
-          - ""
+          - ''
       Warnmf:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     hash:
       FNV32a:
@@ -2751,7 +2793,7 @@ tpl:
         Description: FNV32a hashes v using fnv32a algorithm.
         Examples:
         - - '{{ hash.FNV32a "Hugo Rocks!!" }}'
-          - "1515779328"
+          - '1515779328'
       XxHash:
         Aliases:
         - xxhash
@@ -2765,83 +2807,83 @@ tpl:
       Deps:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Generator:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsDevelopment:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsExtended:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsMultiHost:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsMultihost:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsMultilingual:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsProduction:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsServer:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Store:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Version:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       WorkingDir:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     images:
       AutoOrient:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Brightness:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       ColorBalance:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Colorize:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Config:
         Aliases:
@@ -2855,102 +2897,102 @@ tpl:
       Contrast:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Dither:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Filter:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Gamma:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       GaussianBlur:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Grayscale:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Hue:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Invert:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Mask:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Opacity:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Overlay:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Padding:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Pixelate:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Process:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       QR:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Saturation:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Sepia:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Sigmoid:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Text:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       UnsharpMask:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     inflect:
       Humanize:
@@ -2960,7 +3002,7 @@ tpl:
         - v
         Description: |-
           Humanize returns the humanized form of v.
-
+          
           If v is either an integer or a string containing an integer
           value, the behavior is to add the appropriate ordinal.
         Examples:
@@ -3001,12 +3043,12 @@ tpl:
       Batch:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Build:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     lang:
       FormatAccounting:
@@ -3018,7 +3060,7 @@ tpl:
         Description: |-
           FormatAccounting returns the currency representation of number for the given currency and precision
           for the current language in accounting notation.
-
+          
           The return value is formatted with at least two decimal places.
         Examples:
         - - '{{ 512.5032 | lang.FormatAccounting 2 "NOK" }}'
@@ -3032,7 +3074,7 @@ tpl:
         Description: |-
           FormatCurrency returns the currency representation of number for the given currency and precision
           for the current language.
-
+          
           The return value is formatted with at least two decimal places.
         Examples:
         - - '{{ 512.5032 | lang.FormatCurrency 2 "USD" }}'
@@ -3042,34 +3084,24 @@ tpl:
         Args:
         - precision
         - number
-        Description: FormatNumber formats number with the given precision for the
-          current language.
+        Description: FormatNumber formats number with the given precision for the current language.
         Examples:
         - - '{{ 512.5032 | lang.FormatNumber 2 }}'
-          - "512.50"
+          - '512.50'
       FormatNumberCustom:
         Aliases: null
         Args:
         - precision
         - number
         - options
-        Description: |-
-          FormatNumberCustom formats a number with the given precision. The first
-          options parameter is a space-delimited string of characters to represent
-          negativity, the decimal point, and grouping. The default value is `- . ,`.
-          The second options parameter defines an alternate delimiting character.
-
-          Note that numbers are rounded up at 5 or greater.
-          So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.
-
-          For a simpler function that adapts to the current language, see FormatNumber.
+        Description: 'FormatNumberCustom formats a number with the given precision. The first\noptions parameter is a space-delimited string of characters to represent\nnegativity, the decimal point, and grouping. The default value is `- . ,`.\nThe second options parameter defines an alternate delimiting character.\n\nNote that numbers are rounded up at 5 or greater.\nSo, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.\n\nFor a simpler function that adapts to the current language, see FormatNumber.'
         Examples:
         - - '{{ lang.FormatNumberCustom 2 12345.6789 }}'
           - 12,345.68
         - - '{{ lang.FormatNumberCustom 2 12345.6789 "- , ." }}'
           - 12.345,68
         - - '{{ lang.FormatNumberCustom 6 -12345.6789 "- ." }}'
-          - "-12345.678900"
+          - '-12345.678900'
         - - '{{ lang.FormatNumberCustom 0 -12345.6789 "- . ," }}'
           - -12,346
         - - '{{ lang.FormatNumberCustom 0 -12345.6789 "-|.| " "|" }}'
@@ -3090,7 +3122,7 @@ tpl:
       Merge:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Translate:
         Aliases:
@@ -3106,19 +3138,19 @@ tpl:
       Abs:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Abs returns the absolute value of n.
         Examples:
         - - '{{ math.Abs -2.1 }}'
-          - "2.1"
+          - '2.1'
       Acos:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Acos returns the arccosine, in radians, of n.
         Examples:
         - - '{{ math.Acos 1 }}'
-          - "0"
+          - '0'
       Add:
         Aliases:
         - add
@@ -3127,54 +3159,52 @@ tpl:
         Description: Add adds the multivalued addends n1 and n2 or more values.
         Examples:
         - - '{{ add 1 2 }}'
-          - "3"
+          - '3'
       Asin:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Asin returns the arcsine, in radians, of n.
         Examples:
         - - '{{ math.Asin 1 }}'
-          - "1.5707963267948966"
+          - '1.5707963267948966'
       Atan:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Atan returns the arctangent, in radians, of n.
         Examples:
         - - '{{ math.Atan 1 }}'
-          - "0.7853981633974483"
+          - '0.7853981633974483'
       Atan2:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         - m
-        Description: Atan2 returns the arc tangent of n/m, using the signs of the
-          two to determine the quadrant of the return value.
+        Description: Atan2 returns the arc tangent of n/m, using the signs of the two to determine the quadrant of the return value.
         Examples:
         - - '{{ math.Atan2 1 2 }}'
-          - "0.4636476090008061"
+          - '0.4636476090008061'
       Ceil:
         Aliases: null
         Args:
-        - "n"
-        Description: Ceil returns the least integer value greater than or equal to
-          n.
+        - 'n'
+        Description: Ceil returns the least integer value greater than or equal to n.
         Examples:
         - - '{{ math.Ceil 2.1 }}'
-          - "3"
+          - '3'
       Cos:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Cos returns the cosine of the radian argument n.
         Examples:
         - - '{{ math.Cos 1 }}'
-          - "0.5403023058681398"
+          - '0.5403023058681398'
       Counter:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Div:
         Aliases:
@@ -3184,49 +3214,46 @@ tpl:
         Description: Div divides n1 by n2.
         Examples:
         - - '{{ div 6 3 }}'
-          - "2"
+          - '2'
       Floor:
         Aliases: null
         Args:
-        - "n"
-        Description: Floor returns the greatest integer value less than or equal to
-          n.
+        - 'n'
+        Description: Floor returns the greatest integer value less than or equal to n.
         Examples:
         - - '{{ math.Floor 1.9 }}'
-          - "1"
+          - '1'
       Log:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Log returns the natural logarithm of the number n.
         Examples:
         - - '{{ math.Log 1 }}'
-          - "0"
+          - '0'
       Max:
         Aliases: null
         Args:
         - inputs
-        Description: Max returns the greater of all numbers in inputs. Any slices
-          in inputs are flattened.
+        Description: Max returns the greater of all numbers in inputs. Any slices in inputs are flattened.
         Examples:
         - - '{{ math.Max 1 2 }}'
-          - "2"
+          - '2'
       MaxInt64:
         Aliases: null
         Args: null
         Description: MaxInt64 returns the maximum value for a signed 64-bit integer.
         Examples:
         - - '{{ math.MaxInt64 }}'
-          - "9223372036854775807"
+          - '9223372036854775807'
       Min:
         Aliases: null
         Args:
         - inputs
-        Description: Min returns the smaller of all numbers in inputs. Any slices
-          in inputs are flattened.
+        Description: Min returns the smaller of all numbers in inputs. Any slices in inputs are flattened.
         Examples:
         - - '{{ math.Min 1 2 }}'
-          - "1"
+          - '1'
       Mod:
         Aliases:
         - mod
@@ -3236,18 +3263,17 @@ tpl:
         Description: Mod returns n1 % n2.
         Examples:
         - - '{{ mod 15 3 }}'
-          - "0"
+          - '0'
       ModBool:
         Aliases:
         - modBool
         Args:
         - n1
         - n2
-        Description: ModBool returns the boolean of n1 % n2.  If n1 % n2 == 0, return
-          true.
+        Description: ModBool returns the boolean of n1 % n2.  If n1 % n2 == 0, return true.
         Examples:
         - - '{{ modBool 15 3 }}'
-          - "true"
+          - 'true'
       Mul:
         Aliases:
         - mul
@@ -3256,14 +3282,14 @@ tpl:
         Description: Mul multiplies the multivalued numbers n1 and n2 or more values.
         Examples:
         - - '{{ mul 2 3 }}'
-          - "6"
+          - '6'
       Pi:
         Aliases: null
         Args: null
         Description: Pi returns the mathematical constant pi.
         Examples:
         - - '{{ math.Pi }}'
-          - "3.141592653589793"
+          - '3.141592653589793'
       Pow:
         Aliases:
         - pow
@@ -3273,45 +3299,43 @@ tpl:
         Description: Pow returns n1 raised to the power of n2.
         Examples:
         - - '{{ math.Pow 2 3 }}'
-          - "8"
+          - '8'
       Product:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Rand:
         Aliases: null
         Args: null
-        Description: Rand returns, as a float64, a pseudo-random number in the half-open
-          interval [0.0,1.0).
+        Description: Rand returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0).
         Examples:
         - - '{{ math.Rand }}'
-          - "0.6312770459590062"
+          - '0.6312770459590062'
       Round:
         Aliases: null
         Args:
-        - "n"
-        Description: Round returns the integer nearest to n, rounding half away from
-          zero.
+        - 'n'
+        Description: Round returns the integer nearest to n, rounding half away from zero.
         Examples:
         - - '{{ math.Round 1.5 }}'
-          - "2"
+          - '2'
       Sin:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Sin returns the sine of the radian argument n.
         Examples:
         - - '{{ math.Sin 1 }}'
-          - "0.8414709848078965"
+          - '0.8414709848078965'
       Sqrt:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Sqrt returns the square root of the number n.
         Examples:
         - - '{{ math.Sqrt 81 }}'
-          - "9"
+          - '9'
       Sub:
         Aliases:
         - sub
@@ -3320,41 +3344,41 @@ tpl:
         Description: Sub subtracts multivalued.
         Examples:
         - - '{{ sub 3 2 }}'
-          - "1"
+          - '1'
       Sum:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Tan:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: Tan returns the tangent of the radian argument n.
         Examples:
         - - '{{ math.Tan 1 }}'
-          - "1.557407724654902"
+          - '1.557407724654902'
       ToDegrees:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: ToDegrees converts radians into degrees.
         Examples:
         - - '{{ math.ToDegrees 1.5707963267948966 }}'
-          - "90"
+          - '90'
       ToRadians:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         Description: ToRadians converts degrees into radians.
         Examples:
         - - '{{ math.ToRadians 90 }}'
-          - "1.5707963267948966"
+          - '1.5707963267948966'
     openapi3:
       Unmarshal:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: []
     os:
       FileExists:
@@ -3365,7 +3389,7 @@ tpl:
         Description: FileExists checks whether a file exists under the given path.
         Examples:
         - - '{{ fileExists "foo.txt" }}'
-          - "false"
+          - 'false'
       Getenv:
         Aliases:
         - getenv
@@ -3380,8 +3404,7 @@ tpl:
         - readDir
         Args:
         - i
-        Description: ReadDir lists the directory contents relative to the configured
-          WorkingDir.
+        Description: ReadDir lists the directory contents relative to the configured WorkingDir.
         Examples:
         - - '{{ range (readDir "files") }}{{ .Name }}{{ end }}'
           - README.txt
@@ -3400,7 +3423,7 @@ tpl:
       Stat:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     partials:
       Include:
@@ -3435,27 +3458,27 @@ tpl:
       Base:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       BaseName:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Clean:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Dir:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Ext:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Join:
         Aliases: null
@@ -3517,27 +3540,27 @@ tpl:
       Babel:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       ByType:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Concat:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Copy:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       ExecuteAsTemplate:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Fingerprint:
         Aliases:
@@ -3551,7 +3574,7 @@ tpl:
       FromString:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Get:
         Aliases: null
@@ -3564,25 +3587,18 @@ tpl:
       GetMatch:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       GetRemote:
         Aliases: null
         Args:
         - args
-        Description: |-
-          GetRemote gets the URL (via HTTP(s)) in the first argument in args and creates Resource object that can be used for
-          further transformations.
-
-          A second argument may be provided with an option map.
-
-          Note: This method does not return any error as a second return value,
-          for any error situations the error can be checked in .Err.
+        Description: 'GetRemote gets the URL (via HTTP(s)) in the first argument in args and creates Resource object that can be used for\nfurther transformations.\n\nA second argument may be provided with an option map.\n\nNote: This method does not return any error as a second return value,\nfor any error situations the error can be checked in .Err.'
         Examples: []
       Match:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Minify:
         Aliases:
@@ -3596,17 +3612,17 @@ tpl:
       PostCSS:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       PostProcess:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       ToCSS:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
     safe:
       CSS:
@@ -3665,177 +3681,197 @@ tpl:
       AllPages:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Author:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Authors:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       BaseURL:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       BuildDrafts:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       CheckReady:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Config:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Copyright:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Current:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Data:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
+        Examples: null
+      Dimension:
+        Aliases: null
+        Args: null
+        Description: ''
         Examples: null
       ForEeachIdentityByName:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       GetPage:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Home:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Hugo:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       IsMultiLingual:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Key:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Language:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       LanguageCode:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       LanguagePrefix:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Languages:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       LastChange:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Lastmod:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       MainSections:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Menus:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Pages:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Param:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Params:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       RegularPages:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
+        Examples: null
+      Role:
+        Aliases: null
+        Args: null
+        Description: ''
         Examples: null
       Sections:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       ServerPort:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Sites:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Social:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Store:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
+        Examples: null
+      String:
+        Aliases: null
+        Args: null
+        Description: ''
         Examples: null
       Taxonomies:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Title:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
+        Examples: null
+      Version:
+        Aliases: null
+        Args: null
+        Description: ''
         Examples: null
     strings:
       Chomp:
@@ -3843,8 +3879,7 @@ tpl:
         - chomp
         Args:
         - s
-        Description: Chomp returns a copy of s with all trailing newline characters
-          removed.
+        Description: Chomp returns a copy of s with all trailing newline characters removed.
         Examples:
         - - '{{ chomp "<p>Blockhead</p>\n" | safeHTML }}'
           - <p>Blockhead</p>
@@ -3856,25 +3891,24 @@ tpl:
         Description: Contains reports whether substr is in s.
         Examples:
         - - '{{ strings.Contains "abc" "b" }}'
-          - "true"
+          - 'true'
         - - '{{ strings.Contains "abc" "d" }}'
-          - "false"
+          - 'false'
       ContainsAny:
         Aliases: null
         Args:
         - s
         - chars
-        Description: ContainsAny reports whether any Unicode code points in chars
-          are within s.
+        Description: ContainsAny reports whether any Unicode code points in chars are within s.
         Examples:
         - - '{{ strings.ContainsAny "abc" "bcd" }}'
-          - "true"
+          - 'true'
         - - '{{ strings.ContainsAny "abc" "def" }}'
-          - "false"
+          - 'false'
       ContainsNonSpace:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Count:
         Aliases: null
@@ -3886,7 +3920,7 @@ tpl:
           If substr is an empty string, Count returns 1 + the number of Unicode code points in s.
         Examples:
         - - '{{ "aabab" | strings.Count "a" }}'
-          - "3"
+          - '3'
       CountRunes:
         Aliases:
         - countrunes
@@ -3904,7 +3938,7 @@ tpl:
       Diff:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       FindRE:
         Aliases:
@@ -3917,8 +3951,7 @@ tpl:
           FindRE returns a list of strings that match the regular expression. By default all matches
           will be included. The number of matches can be limited with an optional third parameter.
         Examples:
-        - - '{{ findRE "[G|g]o" "Hugo is a static side generator written in Go." 1
-            }}'
+        - - '{{ findRE "[G|g]o" "Hugo is a static side generator written in Go." 1 }}'
           - '[go]'
       FindRESubmatch:
         Aliases:
@@ -3932,13 +3965,12 @@ tpl:
           expression in content. Each element is a slice of strings holding the text
           of the leftmost match of the regular expression and the matches, if any, of
           its subexpressions.
-
+          
           By default all matches will be included. The number of matches can be
           limited with the optional limit parameter. A return value of nil indicates
           no match.
         Examples:
-        - - '{{ findRESubmatch `<a\s*href="(.+?)">(.+?)</a>` `<li><a href="#foo">Foo</a></li>
-            <li><a href="#bar">Bar</a></li>` | print | safeHTML }}'
+        - - '{{ findRESubmatch `<a\s*href="(.+?)">(.+?)</a>` `<li><a href="#foo">Foo</a></li> <li><a href="#bar">Bar</a></li>` | print | safeHTML }}'
           - '[[<a href="#foo">Foo</a> #foo Foo] [<a href="#bar">Bar</a> #bar Bar]]'
       FirstUpper:
         Aliases: null
@@ -3957,9 +3989,9 @@ tpl:
         Description: HasPrefix tests whether the input s begins with prefix.
         Examples:
         - - '{{ hasPrefix "Hugo" "Hu" }}'
-          - "true"
+          - 'true'
         - - '{{ hasPrefix "Hugo" "Fu" }}'
-          - "false"
+          - 'false'
       HasSuffix:
         Aliases:
         - hasSuffix
@@ -3969,16 +4001,15 @@ tpl:
         Description: HasSuffix tests whether the input s begins with suffix.
         Examples:
         - - '{{ hasSuffix "Hugo" "go" }}'
-          - "true"
+          - 'true'
         - - '{{ hasSuffix "Hugo" "du" }}'
-          - "false"
+          - 'false'
       Repeat:
         Aliases: null
         Args:
-        - "n"
+        - 'n'
         - s
-        Description: Repeat returns a new string consisting of n copies of the string
-          s.
+        Description: Repeat returns a new string consisting of n copies of the string s.
         Examples:
         - - '{{ "yo" | strings.Repeat 4 }}'
           - yoyoyoyo
@@ -4006,7 +4037,7 @@ tpl:
         - pattern
         - repl
         - s
-        - "n"
+        - 'n'
         Description: |-
           ReplaceRE returns a copy of s, replacing all matches of the regular
           expression pattern with the replacement text repl. The number of replacements
@@ -4043,8 +4074,7 @@ tpl:
         Args:
         - a
         - delimiter
-        Description: Split slices an input string into all substrings separated by
-          delimiter.
+        Description: Split slices an input string into all substrings separated by delimiter.
         Examples: []
       Substr:
         Aliases:
@@ -4052,19 +4082,7 @@ tpl:
         Args:
         - a
         - nums
-        Description: |-
-          Substr extracts parts of a string, beginning at the character at the specified
-          position, and returns the specified number of characters.
-
-          It normally takes two parameters: start and length.
-          It can also take one parameter: start, i.e. length is omitted, in which case
-          the substring starting from start until the end of the string will be returned.
-
-          To extract characters from the end of the string, use a negative start number.
-
-          In addition, borrowing from the extended behavior described at http://php.net/substr,
-          if length is given and is negative, then that many characters will be omitted from
-          the end of string.
+        Description: 'Substr extracts parts of a string, beginning at the character at the specified\nposition, and returns the specified number of characters.\n\nIt normally takes two parameters: start and length.\nIt can also take one parameter: start, i.e. length is omitted, in which case\nthe substring starting from start until the end of the string will be returned.\n\nTo extract characters from the end of the string, use a negative start number.\n\nIn addition, borrowing from the extended behavior described at http://php.net/substr,\nif length is given and is negative, then that many characters will be omitted from\nthe end of string.'
         Examples:
         - - '{{ substr "BatMan" 0 -3 }}'
           - Bat
@@ -4155,7 +4173,7 @@ tpl:
       TrimSpace:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       TrimSuffix:
         Aliases: null
@@ -4186,7 +4204,7 @@ tpl:
       Current:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Defer:
         Aliases: null
@@ -4216,8 +4234,7 @@ tpl:
         Examples:
         - - '{{ if (templates.Exists "partials/header.html") }}Yes!{{ end }}'
           - Yes!
-        - - '{{ if not (templates.Exists "partials/doesnotexist.html") }}No!{{ end
-            }}'
+        - - '{{ if not (templates.Exists "partials/doesnotexist.html") }}No!{{ end }}'
           - No!
     time:
       AsTime:
@@ -4230,7 +4247,7 @@ tpl:
           a time.Time interface.
         Examples:
         - - '{{ (time "2015-01-21").Year }}'
-          - "2015"
+          - '2015'
       Duration:
         Aliases:
         - duration
@@ -4258,7 +4275,7 @@ tpl:
       In:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Now:
         Aliases:
@@ -4270,13 +4287,7 @@ tpl:
         Aliases: null
         Args:
         - s
-        Description: |-
-          ParseDuration parses the duration string s.
-          A duration string is a possibly signed sequence of
-          decimal numbers, each with optional fraction and a unit suffix,
-          such as "300ms", "-1.5h" or "2h45m".
-          Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
-          See https://golang.org/pkg/time/#ParseDuration
+        Description: 'ParseDuration parses the duration string s.\nA duration string is a possibly signed sequence of\ndecimal numbers, each with optional fraction and a unit suffix,\nsuch as "300ms", "-1.5h" or "2h45m".\nValid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".\nSee https://golang.org/pkg/time/#ParseDuration'
         Examples:
         - - '{{ "1h12m10s" | time.ParseDuration }}'
           - 1h12m10s
@@ -4284,7 +4295,7 @@ tpl:
       CanHighlight:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Emojify:
         Aliases:
@@ -4293,7 +4304,7 @@ tpl:
         - s
         Description: |-
           Emojify returns a copy of s with all emoji codes replaced with actual emojis.
-
+          
           See http://www.emoji-cheat-sheet.com/
         Examples:
         - - '{{ "I :heart: Hugo" | emojify }}'
@@ -4303,17 +4314,19 @@ tpl:
         - htmlEscape
         Args:
         - s
-        Description: HTMLEscape returns a copy of s with reserved HTML characters
-          escaped.
+        Description: HTMLEscape returns a copy of s with reserved HTML characters escaped.
         Examples:
-        - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" |
-            safeHTML }}'
+        - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | safeHTML }}'
           - Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;
         - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" }}'
           - Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;
-        - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" |
-            htmlUnescape | safeHTML }}'
+        - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | htmlUnescape | safeHTML }}'
           - Cathal Garvey & The Sunshine Band <cathal@foo.bar>
+      HTMLToMarkdown:
+        Aliases: null
+        Args: null
+        Description: ''
+        Examples: null
       HTMLUnescape:
         Aliases:
         - htmlUnescape
@@ -4323,17 +4336,13 @@ tpl:
           HTMLUnescape returns a copy of s with HTML escape requences converted to plain
           text.
         Examples:
-        - - '{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;"
-            | safeHTML }}'
+        - - '{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | safeHTML }}'
           - Cathal Garvey & The Sunshine Band <cathal@foo.bar>
-        - - '{{ "Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;"
-            | htmlUnescape | htmlUnescape | safeHTML }}'
+        - - '{{ "Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;" | htmlUnescape | htmlUnescape | safeHTML }}'
           - Cathal Garvey & The Sunshine Band <cathal@foo.bar>
-        - - '{{ "Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;"
-            | htmlUnescape | htmlUnescape }}'
+        - - '{{ "Cathal Garvey &amp;amp; The Sunshine Band &amp;lt;cathal@foo.bar&amp;gt;" | htmlUnescape | htmlUnescape }}'
           - Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;
-        - - '{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;"
-            | htmlEscape | safeHTML }}'
+        - - '{{ htmlUnescape "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlEscape | safeHTML }}'
           - Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;
       Highlight:
         Aliases:
@@ -4349,7 +4358,7 @@ tpl:
       HighlightCodeBlock:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Markdownify:
         Aliases:
@@ -4373,7 +4382,7 @@ tpl:
       PortableText:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Remarshal:
         Aliases: null
@@ -4388,16 +4397,12 @@ tpl:
           change without notice if it serves a purpose in the docs.
           Format is one of json, yaml or toml.
         Examples:
-        - - '{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML
-            }}'
-          - |
-            {
-               "title": "Hello World"
-            }
+        - - '{{ "title = \"Hello World\"" | transform.Remarshal "json" | safeHTML }}'
+          - '{\n   "title": "Hello World"\n}\n'
       ToMath:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Unmarshal:
         Aliases:
@@ -4411,8 +4416,7 @@ tpl:
         Examples:
         - - '{{ "hello = \"Hello World\"" | transform.Unmarshal }}'
           - map[hello:Hello World]
-        - - '{{ "hello = \"Hello World\"" | resources.FromString "data/greetings.toml"
-            | transform.Unmarshal }}'
+        - - '{{ "hello = \"Hello World\"" | resources.FromString "data/greetings.toml" | transform.Unmarshal }}'
           - map[hello:Hello World]
       XMLEscape:
         Aliases: null
@@ -4469,7 +4473,7 @@ tpl:
       Parse:
         Aliases: null
         Args: null
-        Description: ""
+        Description: ''
         Examples: null
       Ref:
         Aliases:
@@ -4477,8 +4481,7 @@ tpl:
         Args:
         - p
         - args
-        Description: Ref returns the absolute URL path to a given content item from
-          Page p.
+        Description: Ref returns the absolute URL path to a given content item from Page p.
         Examples: []
       RelLangURL:
         Aliases:
@@ -4495,8 +4498,7 @@ tpl:
         Args:
         - p
         - args
-        Description: RelRef returns the relative URL path to a given content item
-          from Page p.
+        Description: RelRef returns the relative URL path to a given content item from Page p.
         Examples: []
       RelURL:
         Aliases:
index 2043b1a4b1ef61254d28a9dda4f66769938a3101..74e349424c310b3dd213e2cb949a52b706e8198a 100644 (file)
@@ -121,7 +121,7 @@ func IsGitHubAction() bool {
 }
 
 // SupportsAll reports whether the running system supports all Hugo features,
-// e.g. Asciidoc, Pandoc etc.
+// e.g. AsciiDoc, Pandoc etc.
 func SupportsAll() bool {
        return IsGitHubAction() || os.Getenv("CI_LOCAL") != ""
 }
index 3f6aa93c40c02b4c4ab6a272ee0c164b78fce8d2..0f7ac65e6c5a6f0bed0133d74416d7050bdb1c3a 100644 (file)
@@ -345,7 +345,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
                shouldExecute func() bool
        }{
                {"md", func() bool { return true }},
-               {"ad", func() bool { return asciidocext.Supports() }},
+               {"ad", func() bool { ok, _ := asciidocext.Supports(); return ok }},
                {"rst", func() bool { return !htesting.IsRealCI() && rst.Supports() }},
        }
 
@@ -574,7 +574,7 @@ func TestPageSummary(t *testing.T) {
        assertFunc := func(t *testing.T, ext string, pages page.Pages) {
                p := pages[0]
                checkPageTitle(t, p, "SimpleWithoutSummaryDelimiter")
-               // Source is not Asciidoctor- or RST-compatible so don't test them
+               // Source is not AsciiDoc- or RST-compatible so don't test them
                if ext != "ad" && ext != "rst" {
                        checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext)
                        checkPageSummary(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p><p>Additional text.</p>"), ext)
@@ -604,7 +604,7 @@ func TestPageWithSummaryParameter(t *testing.T) {
                p := pages[0]
                checkPageTitle(t, p, "SimpleWithSummaryParameter")
                checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n\n<p>Some more text.</p>\n"), ext)
-               // Summary is not Asciidoctor- or RST-compatible so don't test them
+               // Summary is not AsciiDoc- or RST-compatible so don't test them
                if ext != "ad" && ext != "rst" {
                        checkPageSummary(t, p, normalizeExpected(ext, "Page with summary parameter and <a href=\"http://www.example.com/\">a link</a>"), ext)
                }
index 556fb84a9b83681d9c37ab7fc89d1095133bef56..b0c611c503022a8b02f964f078900f1874fe792c 100644 (file)
@@ -132,7 +132,7 @@ docs/p1/sub/mymixcasetext2.txt
        b.AssertFileContent("public/docs/p3/index.html", "<strong>Hello World Default</strong>")
 }
 
-func TestPagesFromGoTmplAsciidocAndSimilar(t *testing.T) {
+func TestPagesFromGoTmplAsciiDocAndSimilar(t *testing.T) {
        files := `
 -- hugo.toml --
 disableKinds = ["taxonomy", "term", "rss", "sitemap"]
@@ -153,7 +153,7 @@ allow = ['asciidoctor', 'pandoc','rst2html', 'python']
 
        b := hugolib.Test(t, files)
 
-       if asciidocext.Supports() {
+       if ok, _ := asciidocext.Supports(); ok {
                b.AssertFileContent("public/docs/asciidoc/index.html",
                        "Mark my words, <mark>automation is essential</mark>",
                        "Path: /docs/asciidoc|",
@@ -503,7 +503,7 @@ baseURL = "https://example.com"
 func TestPagesFromGoTmplCascade(t *testing.T) {
        t.Parallel()
 
-       files := ` 
+       files := `
 -- hugo.toml --
 disableKinds = ["taxonomy", "term", "rss", "sitemap"]
 baseURL = "https://example.com"
@@ -523,7 +523,7 @@ baseURL = "https://example.com"
 func TestPagesFromGoBuildOptions(t *testing.T) {
        t.Parallel()
 
-       files := ` 
+       files := `
 -- hugo.toml --
 disableKinds = ["taxonomy", "term", "rss", "sitemap"]
 baseURL = "https://example.com"
@@ -644,11 +644,11 @@ func TestPagesFromGoTmplMenusMap(t *testing.T) {
 -- hugo.toml --
 disableKinds = ['rss','section','sitemap','taxonomy','term']
 -- content/_content.gotmpl --
-{{ $menu1 := dict 
+{{ $menu1 := dict
     "parent" "main-page"
     "identifier" "id1"
 }}
-{{ $menu2 := dict 
+{{ $menu2 := dict
     "parent" "main-page"
     "identifier" "id2"
 }}
@@ -859,7 +859,7 @@ Title: {{ .Title }}|Content: {{ .Content }}|
 func TestPagesFromGoTmplHome(t *testing.T) {
        t.Parallel()
 
-       files := ` 
+       files := `
 -- hugo.toml --
 disableKinds = ["taxonomy", "term", "rss", "sitemap"]
 baseURL = "https://example.com"
index c4d7f754fedd0dee586bb5500b7efddbfeb0687b..c79bc5c5783fd3dfaed19469753b5c5abfcbab11 100644 (file)
@@ -1927,9 +1927,9 @@ Myotherpartial Inline.|{{ .Title }}|
        b.AssertFileContent("public/index.html", "My inline partial in all Edited.")
 }
 
-func TestRebuildEditAsciidocContentFile(t *testing.T) {
-       if !asciidocext.Supports() {
-               t.Skip("skip asciidoc")
+func TestRebuildEditAsciiDocContentFile(t *testing.T) {
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
        files := `
 -- hugo.toml --
index 57bf302a48ba43bc892c2f221a4a50835ec4afed..a7639be8b2aa4f9aa407d111d3c522b6851116fa 100644 (file)
@@ -54,10 +54,10 @@ baseURL = "https://example.org"
                Test(c, files)
        })
 
-       c.Run("Asciidoc, denied", func(c *qt.C) {
+       c.Run("AsciiDoc, denied", func(c *qt.C) {
                c.Parallel()
-               if !asciidocext.Supports() {
-                       c.Skip()
+               if ok, err := asciidocext.Supports(); !ok {
+                       c.Skip(err)
                }
 
                files := `
diff --git a/markup/asciidocext/asciidoc_integration_test.go b/markup/asciidocext/asciidoc_integration_test.go
new file mode 100644 (file)
index 0000000..55c8985
--- /dev/null
@@ -0,0 +1,1155 @@
+// Copyright 2025 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 asciidocext_test
+
+import (
+       "fmt"
+       "os"
+       "path/filepath"
+       "runtime"
+       "slices"
+       "strings"
+       "testing"
+
+       "github.com/gohugoio/hugo/htesting"
+       "github.com/gohugoio/hugo/hugolib"
+       "github.com/gohugoio/hugo/markup/asciidocext"
+)
+
+type contentFile struct {
+       // The path to the content file, relative to the project directory.
+       path string
+       // The title of the content file.
+       title string
+}
+
+type publishedContentFile struct {
+       // The path to the published content file, relative to the project directory.
+       path string
+       // The expected string in the content file used for content test assertions.
+       match string
+}
+
+type testCase struct {
+       name                           string
+       baseURL                        string
+       defaultContentLanguageInSubdir bool
+       langEnBaseURL                  string
+       langDeBaseURL                  string
+       langDeDisabled                 bool
+       uglyURLsS1                     bool
+       validate                       func(b *hugolib.IntegrationTestBuilder)
+}
+
+// Issue 9202, Issue 10183, Issue 10473
+func TestAsciiDocDiagrams(t *testing.T) {
+       if !htesting.IsRealCI() {
+               t.Skip()
+       }
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
+       }
+       if ok, err := asciidocext.SupportsGoATDiagrams(); !ok {
+               t.Skip(err)
+       }
+
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
+
+       diagramCacheDir := t.TempDir() // we want this to persist for all tests
+
+       files := createContentFiles() + contentAdapter + layouts
+
+       testCases := []testCase{
+               // Test 1 - Monolingual, uglyURLs = false for s1, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 1", "https://example.org/", false, "", "", true, false, validatePublishedSite_1},
+               // Test 2 - Monolingual, uglyURLs = false for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 2", "https://example.org/subdir/", false, "", "", true, false, validatePublishedSite_2},
+               // // Test 3 - Monolingual, uglyURLs = true for s1, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 3", "https://example.org/", false, "", "", true, true, validatePublishedSite_3},
+               // // Test 4 - Monolingual, uglyURLs = true for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 4", "https://example.org/subdir/", false, "", "", true, true, validatePublishedSite_4},
+               // // Test 5 - Multilingual, single-host, uglyURLs = false for s1, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 5", "https://example.org/", false, "", "", false, false, validatePublishedSite_5},
+               // Test 6 - Multilingual, single-host, uglyURLs = false for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 6", "https://example.org/subdir/", false, "", "", false, false, validatePublishedSite_6},
+               // Test 7 - Multilingual, single-host, uglyURLs = true for s1, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 7", "https://example.org/", false, "", "", false, true, validatePublishedSite_7},
+               // Test 8 - Multilingual, single-host, uglyURLs = true for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 8", "https://example.org/subdir/", false, "", "", false, true, validatePublishedSite_8},
+               // Test 9 - Multilingual, single-host, uglyURLs = false for s1, without subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 9", "https://example.org/", true, "", "", false, false, validatePublishedSite_9},
+               // Test 10 - Multilingual, single-host, uglyURLs = false for s1, with subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 10", "https://example.org/subdir/", true, "", "", false, false, validatePublishedSite_10},
+               // Test 11 - Multilingual, single-host, uglyURLs = true for s1, without subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 11", "https://example.org/", true, "", "", false, true, validatePublishedSite_11},
+               // Test 12 - Multilingual, single-host, uglyURLs = true for s1, with subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 12", "https://example.org/subdir/", true, "", "", false, true, validatePublishedSite_12},
+               // Test 13 - Multilingual, multi-host, uglyURLs = false, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 13", "", false, `baseURL = "https://.en.example.org/"`, `baseURL = "https://.de.example.org/"`, false, false, validatePublishedSite_13},
+               // Test 14 - Multilingual, multi-host, uglyURLs = false for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 14", "", false, `baseURL = "https://.en.example.org/subdir/"`, `baseURL = "https://.de.example.org/subdir/"`, false, false, validatePublishedSite_14},
+               // Test 15 - Multilingual, multi-host, uglyURLs = true for s1, without subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 15", "", false, `baseURL = "https://.en.example.org/"`, `baseURL = "https://.de.example.org/"`, false, true, validatePublishedSite_15},
+               // Test 16 - Multilingual, multi-host, uglyURLs = true for s1, with subdir in base URL, defaultContentLanguageInSubdir = false
+               {"Test 16", "", false, `baseURL = "https://.en.example.org/subdir/"`, `baseURL = "https://.de.example.org/subdir/"`, false, true, validatePublishedSite_16},
+               // Test 17 - Multilingual, multi-host, uglyURLs = false for s1, without subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 17", "", true, `baseURL = "https://.en.example.org/"`, `baseURL = "https://.de.example.org/"`, false, false, validatePublishedSite_17},
+               // Test 18 - Multilingual, multi-host, uglyURLs = false for s1, with subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 18", "", true, `baseURL = "https://.en.example.org/subdir/"`, `baseURL = "https://.de.example.org/subdir/"`, false, false, validatePublishedSite_18},
+               // Test 19 - Multilingual, multi-host, uglyURLs = true for s1, without subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 19", "", true, `baseURL = "https://.en.example.org/"`, `baseURL = "https://.de.example.org/"`, false, true, validatePublishedSite_19},
+               // Test 20 - Multilingual, multi-host, uglyURLs = true for s1, with subdir in base URL, defaultContentLanguageInSubdir = true
+               {"Test 20", "", true, `baseURL = "https://.en.example.org/subdir/"`, `baseURL = "https://.de.example.org/subdir/"`, false, true, validatePublishedSite_20},
+       }
+
+       for _, tc := range testCases {
+               // Asciidoctor is really, really slow on Windows. Only run a few tests.
+               isExcludedTest := htesting.IsRealCI() && runtime.GOOS == "windows" && !slices.Contains([]string{"Test 4", "Test 8", "Test 16"}, tc.name)
+               if isExcludedTest {
+                       continue
+               }
+
+               t.Run(tc.name, func(t *testing.T) {
+                       baseURLKV := ""
+                       if tc.baseURL != "" {
+                               baseURLKV = fmt.Sprintf(`baseURL = %q`, tc.baseURL)
+                       }
+
+                       replacer := strings.NewReplacer(
+                               `BASE_URL_KEY_VALUE_PAIR`, baseURLKV,
+                               `DEFAULT_CONTENT_LANGUAGE_IN_SUBDIR`, fmt.Sprintf("%v", tc.defaultContentLanguageInSubdir),
+                               `LANGUAGES.EN.BASE_URL_KEY_VALUE_PAIR`, tc.langEnBaseURL,
+                               `LANGUAGES.DE.BASE_URL_KEY_VALUE_PAIR`, tc.langDeBaseURL,
+                               `LANGUAGES.DE.DISABLED`, fmt.Sprintf("%v", tc.langDeDisabled),
+                               `UGLYURLS_S1`, fmt.Sprintf("%v", tc.uglyURLsS1),
+                               `DIAGRAM_CACHEDIR`, diagramCacheDir,
+                       )
+
+                       f := files + replacer.Replace(configFileWithPlaceholders)
+
+                       tempDir := t.TempDir()
+                       t.Chdir(tempDir)
+
+                       b := hugolib.Test(t, f, hugolib.TestOptWithConfig(func(cfg *hugolib.IntegrationTestConfig) {
+                               cfg.NeedsOsFS = true
+                               cfg.WorkingDir = tempDir
+                       }))
+
+                       // Verify that asciidoctor-diagram is caching in the correct
+                       // location. Checking one file is sufficient.
+                       err := fileExistsOsFs(filepath.Join(diagramCacheDir, "filecache/misc/asciidoctor-diagram/home_en.svg"))
+                       if err != nil {
+                               t.Fatalf("unable to locate file in diagram cache: %v", err.Error())
+                       }
+
+                       tc.validate(b)
+               })
+       }
+}
+
+// fileExistsOsFs checks if a file exists on the OS file system.
+func fileExistsOsFs(path string) error {
+       fi, err := os.Stat(path)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       return fmt.Errorf("the path %q does not exist", path)
+               }
+               return fmt.Errorf("unable to get file status for %q: %v", path, err)
+       }
+       if fi.IsDir() {
+               return fmt.Errorf("the path %q is a directory, not a file", path)
+       }
+       return nil
+}
+
+// createContentFiles returns a txtar representation of the content files.
+func createContentFiles() string {
+       contentFiles := []contentFile{
+               {`content/_index.de.adoc`, `home_de`},
+               {`content/_index.en.adoc`, `home_en`},
+               {`content/s1/_index.de.adoc`, `s1_de`},
+               {`content/s1/_index.en.adoc`, `s1_en`},
+               {`content/s1/p1/index.de.adoc`, `s1_p1_de`},
+               {`content/s1/p1/index.en.adoc`, `s1_p1_en`},
+               {`content/s1/p2.de.adoc`, `s1_p2_de`},
+               {`content/s1/p2.en.adoc`, `s1_p2_en`},
+               {`content/s2/_content.gotmpl`, `s2_content_gotmpl`},
+               {`content/s2/_index.de.adoc`, `s2_de`},
+               {`content/s2/_index.en.adoc`, `s2_en`},
+               {`content/Straßen/Frühling/_index.de.adoc`, `Straßen_Frühling_de`},
+               {`content/Straßen/Frühling/_index.en.adoc`, `Straßen_Frühling_en`},
+               {`content/Straßen/Frühling/Müll Brücke.de.adoc`, `Straßen_Frühling_Müll_Brücke_de`},
+               {`content/Straßen/Frühling/Müll Brücke.en.adoc`, `Straßen_Frühling_Müll_Brücke_en`},
+               {`content/Straßen/_index.de.adoc`, `Straßen_de`},
+               {`content/Straßen/_index.en.adoc`, `Straßen_en`},
+       }
+
+       formatString := `
+-- %[1]s --
+---
+title: %[2]s
+---
+[goat,%[2]s]
+....
+%[2]s
+....`
+
+       var txtarContentFiles strings.Builder
+       for _, f := range contentFiles {
+               txtarContentFiles.WriteString(fmt.Sprintf(formatString, f.path, f.title))
+       }
+
+       return txtarContentFiles.String()
+}
+
+var contentAdapter = `
+-- content/s2/_content.gotmpl --
+{{ $title := printf "s2_p1_%s" site.Language.Lang }}
+{{ $markup := printf "[goat,%[1]s]\n....\n%[1]s\n...." $title }}
+{{ $content := dict
+  "mediaType" "text/asciidoc"
+  "value" $markup
+}}
+{{ $page := dict
+  "content" $content
+  "kind" "page"
+  "path" "p1"
+  "title" $title
+}}
+{{ .EnableAllLanguages }}
+{{ .AddPage $page }}`
+
+var layouts = `
+-- layouts/all.html --
+{{ .Content }}|`
+
+var configFileWithPlaceholders = `
+-- hugo.toml --
+BASE_URL_KEY_VALUE_PAIR
+disableKinds = ['rss', 'sitemap', 'taxonomy', 'term']
+defaultContentLanguage = 'en'
+defaultContentLanguageInSubdir = DEFAULT_CONTENT_LANGUAGE_IN_SUBDIR
+
+[languages.en]
+  LANGUAGES.EN.BASE_URL_KEY_VALUE_PAIR
+  disabled = false
+  weight = 1
+
+[languages.de]
+  LANGUAGES.DE.BASE_URL_KEY_VALUE_PAIR
+  disabled = LANGUAGES.DE.DISABLED
+  weight = 2
+
+[uglyurls]
+  s1 = UGLYURLS_S1
+
+[markup.asciidocext]
+  extensions           = ['asciidoctor-diagram']
+  workingFolderCurrent = true
+
+[caches.misc]
+dir = 'DIAGRAM_CACHEDIR'
+
+[security.exec]
+  allow = ['^((dart-)?sass|git|go|npx|postcss|tailwindcss|asciidoctor)$']`
+
+func validatePublishedSite_1(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_2(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_3(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_4(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_5(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_6(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_7(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_8(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/index.html`, `src="home_en.svg"`},
+               {`public/s1/index.html`, `src="s1_en.svg"`},
+               {`public/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/s2/index.html`, `src="s2_en.svg"`},
+               {`public/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/home_en.svg`,
+               `public/s1/p1/s1_p1_en.svg`,
+               `public/s1/p2/s1_p2_en.svg`,
+               `public/s1/s1_en.svg`,
+               `public/s2/p1/s2_p1_en.svg`,
+               `public/s2/s2_en.svg`,
+               `public/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_9(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_10(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_11(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_12(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_13(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_14(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_15(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_16(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_17(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_18(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1/index.html`, `src="s1_p1_de.svg"`},
+               {`public/de/s1/p2/index.html`, `src="s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1/index.html`, `src="s1_p1_en.svg"`},
+               {`public/en/s1/p2/index.html`, `src="s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_19(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
+
+func validatePublishedSite_20(b *hugolib.IntegrationTestBuilder) {
+       for _, file := range []publishedContentFile{
+               {`public/de/index.html`, `src="home_de.svg"`},
+               {`public/de/s1/index.html`, `src="s1_de.svg"`},
+               {`public/de/s1/p1.html`, `src="p1/s1_p1_de.svg"`},
+               {`public/de/s1/p2.html`, `src="p2/s1_p2_de.svg"`},
+               {`public/de/s2/index.html`, `src="s2_de.svg"`},
+               {`public/de/s2/p1/index.html`, `src="s2_p1_de.svg"`},
+               {`public/de/straßen/frühling/index.html`, `src="Straßen_Frühling_de.svg"`},
+               {`public/de/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_de.svg"`},
+               {`public/de/straßen/index.html`, `src="Straßen_de.svg"`},
+               {`public/en/index.html`, `src="home_en.svg"`},
+               {`public/en/s1/index.html`, `src="s1_en.svg"`},
+               {`public/en/s1/p1.html`, `src="p1/s1_p1_en.svg"`},
+               {`public/en/s1/p2.html`, `src="p2/s1_p2_en.svg"`},
+               {`public/en/s2/index.html`, `src="s2_en.svg"`},
+               {`public/en/s2/p1/index.html`, `src="s2_p1_en.svg"`},
+               {`public/en/straßen/frühling/index.html`, `src="Straßen_Frühling_en.svg"`},
+               {`public/en/straßen/frühling/müll-brücke/index.html`, `src="Straßen_Frühling_Müll_Brücke_en.svg"`},
+               {`public/en/straßen/index.html`, `src="Straßen_en.svg"`},
+       } {
+               b.AssertFileContent(file.path, file.match)
+       }
+
+       for _, path := range []string{
+               `public/de/home_de.svg`,
+               `public/de/s1/p1/s1_p1_de.svg`,
+               `public/de/s1/p2/s1_p2_de.svg`,
+               `public/de/s1/s1_de.svg`,
+               `public/de/s2/p1/s2_p1_de.svg`,
+               `public/de/s2/s2_de.svg`,
+               `public/de/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_de.svg`,
+               `public/de/straßen/frühling/Straßen_Frühling_de.svg`,
+               `public/de/straßen/Straßen_de.svg`,
+               `public/en/home_en.svg`,
+               `public/en/s1/p1/s1_p1_en.svg`,
+               `public/en/s1/p2/s1_p2_en.svg`,
+               `public/en/s1/s1_en.svg`,
+               `public/en/s2/p1/s2_p1_en.svg`,
+               `public/en/s2/s2_en.svg`,
+               `public/en/straßen/frühling/müll-brücke/Straßen_Frühling_Müll_Brücke_en.svg`,
+               `public/en/straßen/frühling/Straßen_Frühling_en.svg`,
+               `public/en/straßen/Straßen_en.svg`,
+       } {
+               b.AssertFileExists(path, true)
+       }
+}
index ecf3eb9acf99f5bd133f26cb15bde1e178bc982a..e8d59a521f2b1e93b7857562a89b3d2aa91843e5 100644 (file)
@@ -29,21 +29,38 @@ type provider struct{}
 
 func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
        return converter.NewProvider("asciidocext", func(ctx converter.DocumentContext) (converter.Converter, error) {
-               return &internal.AsciidocConverter{
+               return &internal.AsciiDocConverter{
                        Ctx: ctx,
                        Cfg: cfg,
                }, nil
        }), nil
 }
 
-// Supports returns whether Asciidoctor is installed on this computer.
-func Supports() bool {
-       hasBin := internal.HasAsciiDoc()
+// Supports reports whether the AsciiDoc converter is installed. Only used in
+// tests.
+func Supports() (bool, error) {
+       hasAsciiDoc, err := internal.HasAsciiDoc()
        if htesting.SupportsAll() {
-               if !hasBin {
-                       panic("asciidoctor not installed")
+               if !hasAsciiDoc {
+                       panic(err)
                }
-               return true
+               return true, nil
+
+       }
+
+       return hasAsciiDoc, err
+}
+
+// SupportsGoATDiagrams reports whether the AsciiDoc converter can render GoAT
+// diagrams. Only used in tests.
+func SupportsGoATDiagrams() (bool, error) {
+       supportsGoATDiagrams, err := internal.CanRenderGoATDiagrams()
+       if htesting.SupportsAll() {
+               if !supportsGoATDiagrams {
+                       panic(err)
+               }
+               return true, nil
        }
-       return hasBin
+
+       return supportsGoATDiagrams, err
 }
index bfcd4ae8fd3b08381b4d6d023e3a29d327c5463f..6166db069590d356dd789b736026e7c5db3f1a84 100644 (file)
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package asciidocext converts AsciiDoc to HTML using Asciidoctor
+// Package asciidocext converts AsciiDoc to HTML using the Asciidoctor
 // external binary. The `asciidoc` module is reserved for a future golang
 // implementation.
 
 package asciidocext_test
 
 import (
-       "path/filepath"
+       "encoding/json"
        "testing"
 
        "github.com/gohugoio/hugo/common/collections"
@@ -28,6 +28,7 @@ import (
        "github.com/gohugoio/hugo/config/security"
        "github.com/gohugoio/hugo/config/testconfig"
        "github.com/gohugoio/hugo/markup/asciidocext"
+       "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
        "github.com/gohugoio/hugo/markup/asciidocext/internal"
        "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/markup/markup_config"
@@ -36,6 +37,24 @@ import (
        qt "github.com/frankban/quicktest"
 )
 
+var defaultAsciiDocExtConfigAsJSON []byte
+
+func init() {
+       var err error
+       defaultAsciiDocExtConfigAsJSON, err = json.Marshal(markup_config.Default.AsciiDocExt)
+       if err != nil {
+               panic(err)
+       }
+}
+
+func resetDefaultAsciiDocExtConfig() {
+       markup_config.Default.AsciiDocExt = asciidocext_config.Config{}
+       err := json.Unmarshal(defaultAsciiDocExtConfigAsJSON, &markup_config.Default.AsciiDocExt)
+       if err != nil {
+               panic(err)
+       }
+}
+
 func TestAsciidoctorDefaultArgs(t *testing.T) {
        c := qt.New(t)
        cfg := config.New()
@@ -52,24 +71,30 @@ func TestAsciidoctorDefaultArgs(t *testing.T) {
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
-       ac := conv.(*internal.AsciidocConverter)
+       ac := conv.(*internal.AsciiDocConverter)
        c.Assert(ac, qt.Not(qt.IsNil))
 
-       args := ac.ParseArgs(converter.DocumentContext{})
+       args, err := ac.ParseArgs(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
        expected := []string{"--no-header-footer"}
        c.Assert(args, qt.DeepEquals, expected)
 }
 
 func TestAsciidoctorNonDefaultArgs(t *testing.T) {
        c := qt.New(t)
+
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
+
        mconf := markup_config.Default
-       mconf.AsciidocExt.Backend = "manpage"
-       mconf.AsciidocExt.NoHeaderOrFooter = false
-       mconf.AsciidocExt.SafeMode = "safe"
-       mconf.AsciidocExt.SectionNumbers = true
-       mconf.AsciidocExt.Verbose = true
-       mconf.AsciidocExt.Trace = false
-       mconf.AsciidocExt.FailureLevel = "warn"
+       mconf.AsciiDocExt.Backend = "manpage"
+       mconf.AsciiDocExt.NoHeaderOrFooter = false
+       mconf.AsciiDocExt.SafeMode = "safe"
+       mconf.AsciiDocExt.SectionNumbers = true
+       mconf.AsciiDocExt.Verbose = true
+       mconf.AsciiDocExt.Trace = false
+       mconf.AsciiDocExt.FailureLevel = "warn"
 
        conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
 
@@ -84,22 +109,28 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) {
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
-       ac := conv.(*internal.AsciidocConverter)
+       ac := conv.(*internal.AsciiDocConverter)
        c.Assert(ac, qt.Not(qt.IsNil))
 
-       args := ac.ParseArgs(converter.DocumentContext{})
+       args, err := ac.ParseArgs(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
        expected := []string{"-b", "manpage", "--section-numbers", "--verbose", "--failure-level", "warn", "--safe-mode", "safe"}
        c.Assert(args, qt.DeepEquals, expected)
 }
 
 func TestAsciidoctorDisallowedArgs(t *testing.T) {
        c := qt.New(t)
+
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
+
        mconf := markup_config.Default
-       mconf.AsciidocExt.Backend = "disallowed-backend"
-       mconf.AsciidocExt.Extensions = []string{"./disallowed-extension"}
-       mconf.AsciidocExt.Attributes = map[string]any{"outdir": "disallowed-attribute"}
-       mconf.AsciidocExt.SafeMode = "disallowed-safemode"
-       mconf.AsciidocExt.FailureLevel = "disallowed-failurelevel"
+       mconf.AsciiDocExt.Backend = "disallowed-backend"
+       mconf.AsciiDocExt.Extensions = []string{"./disallowed-extension"}
+       mconf.AsciiDocExt.Attributes = map[string]any{"outdir": "disallowed-attribute"}
+       mconf.AsciiDocExt.SafeMode = "disallowed-safemode"
+       mconf.AsciiDocExt.FailureLevel = "disallowed-failurelevel"
 
        conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
 
@@ -114,18 +145,24 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) {
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
-       ac := conv.(*internal.AsciidocConverter)
+       ac := conv.(*internal.AsciiDocConverter)
        c.Assert(ac, qt.Not(qt.IsNil))
 
-       args := ac.ParseArgs(converter.DocumentContext{})
+       args, err := ac.ParseArgs(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
        expected := []string{"--no-header-footer"}
        c.Assert(args, qt.DeepEquals, expected)
 }
 
 func TestAsciidoctorArbitraryExtension(t *testing.T) {
        c := qt.New(t)
+
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
+
        mconf := markup_config.Default
-       mconf.AsciidocExt.Extensions = []string{"arbitrary-extension"}
+       mconf.AsciiDocExt.Extensions = []string{"arbitrary-extension"}
        conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
        p, err := asciidocext.Provider.New(
                converter.ProviderConfig{
@@ -138,10 +175,11 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) {
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
-       ac := conv.(*internal.AsciidocConverter)
+       ac := conv.(*internal.AsciiDocConverter)
        c.Assert(ac, qt.Not(qt.IsNil))
 
-       args := ac.ParseArgs(converter.DocumentContext{})
+       args, err := ac.ParseArgs(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
        expected := []string{"-r", "arbitrary-extension", "--no-header-footer"}
        c.Assert(args, qt.DeepEquals, expected)
 }
@@ -149,6 +187,10 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) {
 func TestAsciidoctorDisallowedExtension(t *testing.T) {
        c := qt.New(t)
 
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
+
        for _, disallowedExtension := range []string{
                `foo-bar//`,
                `foo-bar\\ `,
@@ -159,7 +201,7 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) {
                `foo.bar`,
        } {
                mconf := markup_config.Default
-               mconf.AsciidocExt.Extensions = []string{disallowedExtension}
+               mconf.AsciiDocExt.Extensions = []string{disallowedExtension}
                conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf)
                p, err := asciidocext.Provider.New(
                        converter.ProviderConfig{
@@ -172,94 +214,23 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) {
                conv, err := p.New(converter.DocumentContext{})
                c.Assert(err, qt.IsNil)
 
-               ac := conv.(*internal.AsciidocConverter)
+               ac := conv.(*internal.AsciiDocConverter)
                c.Assert(ac, qt.Not(qt.IsNil))
 
-               args := ac.ParseArgs(converter.DocumentContext{})
+               args, err := ac.ParseArgs(converter.DocumentContext{})
+               c.Assert(err, qt.IsNil)
                expected := []string{"--no-header-footer"}
                c.Assert(args, qt.DeepEquals, expected)
        }
 }
 
-func TestAsciidoctorWorkingFolderCurrent(t *testing.T) {
-       c := qt.New(t)
-       cfg := config.FromTOMLConfigString(`
-[markup]
-[markup.asciidocext]
-workingFolderCurrent = true
-trace = false
-`)
-
-       conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg)
-
-       p, err := asciidocext.Provider.New(
-               converter.ProviderConfig{
-                       Conf:   conf,
-                       Logger: loggers.NewDefault(),
-               },
-       )
-       c.Assert(err, qt.IsNil)
-
-       ctx := converter.DocumentContext{Filename: "/tmp/hugo_asciidoc_ddd/docs/chapter2/index.adoc", DocumentName: "chapter2/index.adoc"}
-       conv, err := p.New(ctx)
-       c.Assert(err, qt.IsNil)
-
-       ac := conv.(*internal.AsciidocConverter)
-       c.Assert(ac, qt.Not(qt.IsNil))
-
-       args := ac.ParseArgs(ctx)
-       c.Assert(len(args), qt.Equals, 5)
-       c.Assert(args[0], qt.Equals, "--base-dir")
-       c.Assert(filepath.ToSlash(args[1]), qt.Matches, "/tmp/hugo_asciidoc_ddd/docs/chapter2")
-       c.Assert(args[2], qt.Equals, "-a")
-       c.Assert(args[3], qt.Matches, `outdir=.*chapter2`)
-       c.Assert(args[4], qt.Equals, "--no-header-footer")
-}
-
-func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) {
+func TestAsciidoctorAttributes(t *testing.T) {
        c := qt.New(t)
-       cfg := config.FromTOMLConfigString(`
-[markup]
-[markup.asciidocext]
-backend = "html5s"
-workingFolderCurrent = true
-trace = false
-noHeaderOrFooter = true
-extensions = ["asciidoctor-html5s", "asciidoctor-diagram"]
-`)
-       conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg)
 
-       p, err := asciidocext.Provider.New(
-               converter.ProviderConfig{
-                       Conf:   conf,
-                       Logger: loggers.NewDefault(),
-               },
-       )
-       c.Assert(err, qt.IsNil)
-
-       conv, err := p.New(converter.DocumentContext{})
-       c.Assert(err, qt.IsNil)
-
-       ac := conv.(*internal.AsciidocConverter)
-       c.Assert(ac, qt.Not(qt.IsNil))
-
-       args := ac.ParseArgs(converter.DocumentContext{})
-       c.Assert(len(args), qt.Equals, 11)
-       c.Assert(args[0], qt.Equals, "-b")
-       c.Assert(args[1], qt.Equals, "html5s")
-       c.Assert(args[2], qt.Equals, "-r")
-       c.Assert(args[3], qt.Equals, "asciidoctor-html5s")
-       c.Assert(args[4], qt.Equals, "-r")
-       c.Assert(args[5], qt.Equals, "asciidoctor-diagram")
-       c.Assert(args[6], qt.Equals, "--base-dir")
-       c.Assert(args[7], qt.Equals, ".")
-       c.Assert(args[8], qt.Equals, "-a")
-       c.Assert(args[9], qt.Contains, "outdir=")
-       c.Assert(args[10], qt.Equals, "--no-header-footer")
-}
+       t.Cleanup(func() {
+               resetDefaultAsciiDocExtConfig()
+       })
 
-func TestAsciidoctorAttributes(t *testing.T) {
-       c := qt.New(t)
        cfg := config.FromTOMLConfigString(`
 [markup]
 [markup.asciidocext]
@@ -282,7 +253,7 @@ my-attribute-false = false
        conv, err := p.New(converter.DocumentContext{})
        c.Assert(err, qt.IsNil)
 
-       ac := conv.(*internal.AsciidocConverter)
+       ac := conv.(*internal.AsciiDocConverter)
        c.Assert(ac, qt.Not(qt.IsNil))
 
        expectedValues := map[string]bool{
@@ -292,7 +263,8 @@ my-attribute-false = false
                "'!my-attribute-false'":          true,
        }
 
-       args := ac.ParseArgs(converter.DocumentContext{})
+       args, err := ac.ParseArgs(converter.DocumentContext{})
+       c.Assert(err, qt.IsNil)
        c.Assert(len(args), qt.Equals, 9)
        c.Assert(args[0], qt.Equals, "-a")
        c.Assert(expectedValues[args[1]], qt.Equals, true)
@@ -329,8 +301,8 @@ allow = ['asciidoctor']
 }
 
 func TestConvert(t *testing.T) {
-       if !asciidocext.Supports() {
-               t.Skip("asciidoctor not installed")
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
        c := qt.New(t)
 
@@ -345,8 +317,8 @@ func TestConvert(t *testing.T) {
 }
 
 func TestTableOfContents(t *testing.T) {
-       if !asciidocext.Supports() {
-               t.Skip("asciidoctor not installed")
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
        c := qt.New(t)
        p := getProvider(c, "")
@@ -387,8 +359,8 @@ testContent
 }
 
 func TestTableOfContentsWithCode(t *testing.T) {
-       if !asciidocext.Supports() {
-               t.Skip("asciidoctor not installed")
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
        c := qt.New(t)
        p := getProvider(c, "")
@@ -406,8 +378,8 @@ func TestTableOfContentsWithCode(t *testing.T) {
 }
 
 func TestTableOfContentsPreserveTOC(t *testing.T) {
-       if !asciidocext.Supports() {
-               t.Skip("asciidoctor not installed")
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
        c := qt.New(t)
        confStr := `
index 47e4066410f064741b8d52bd641bee2882c3dc10..594f6fbbf67523bbb6a1e6dc4248c24542ef645b 100644 (file)
@@ -2,11 +2,17 @@ package internal
 
 import (
        "bytes"
+       "fmt"
+       "net/url"
+       "path"
        "path/filepath"
        "strings"
 
        "github.com/gohugoio/hugo/common/hexec"
+       "github.com/gohugoio/hugo/common/loggers"
+       "github.com/gohugoio/hugo/config/security"
        "github.com/gohugoio/hugo/identity"
+       "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config"
        "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/markup/internal"
@@ -15,27 +21,52 @@ import (
        "golang.org/x/net/html"
 )
 
-type AsciidocConverter struct {
+type AsciiDocConverter struct {
        Ctx converter.DocumentContext
        Cfg converter.ProviderConfig
 }
 
-type AsciidocResult struct {
+type asciiDocResult struct {
        converter.ResultRender
        toc *tableofcontents.Fragments
 }
 
-/* ToDo: RelPermalink patch for svg posts not working*/
 type pageSubset interface {
+       IsPage() bool
        RelPermalink() string
+       Section() string
 }
 
-func (r AsciidocResult) TableOfContents() *tableofcontents.Fragments {
+const (
+       // asciiDocBinaryName is name of the AsciiDoc converter CLI.
+       asciiDocBinaryName = "asciidoctor"
+
+       // asciiDocDiagramExtension is the name of the AsciiDoc converter diagram
+       // extension.
+       asciiDocDiagramExtension = "asciidoctor-diagram"
+
+       // asciiDocDiagramCacheDirKey is the AsciiDoc converter attribute key for
+       // setting the path to the diagram cache directory.
+       asciiDocDiagramCacheDirKey = "diagram-cachedir"
+
+       // asciiDocDiagramCacheImagesOptionKey is the AsciiDoc converter attribute
+       // key for determining whether to cache image files in addition to
+       // metadata files.
+       asciiDocDiagramCacheImagesOptionKey = "diagram-cache-images-option"
+
+       // gemBinaryName is the name of the RubyGems CLI.
+       gemBinaryName = "gem"
+
+       // goatBinaryName is the name of the GoAT CLI.
+       goatBinaryName = "goat"
+)
+
+func (r asciiDocResult) TableOfContents() *tableofcontents.Fragments {
        return r.toc
 }
 
-func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
-       b, err := a.GetAsciidocContent(ctx.Src, a.Ctx)
+func (a *AsciiDocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) {
+       b, err := a.GetAsciiDocContent(ctx.Src, a.Ctx)
        if err != nil {
                return nil, err
        }
@@ -43,50 +74,81 @@ func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.Resu
        if err != nil {
                return nil, err
        }
-       return AsciidocResult{
+       return asciiDocResult{
                ResultRender: converter.Bytes(content),
                toc:          toc,
        }, nil
 }
 
-func (a *AsciidocConverter) Supports(_ identity.Identity) bool {
+func (a *AsciiDocConverter) Supports(_ identity.Identity) bool {
        return false
 }
 
-// GetAsciidocContent calls asciidoctor as an external helper
-// to convert AsciiDoc content to HTML.
-func (a *AsciidocConverter) GetAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) {
-       if !HasAsciiDoc() {
-               a.Cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n",
-                       "                 Leaving AsciiDoc content unrendered.")
+// GetAsciiDocContent calls asciidoctor as an external helper to convert
+// AsciiDoc content to HTML.
+func (a *AsciiDocConverter) GetAsciiDocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) {
+       if ok, err := HasAsciiDoc(); !ok {
+               a.Cfg.Logger.Errorf("leaving AsciiDoc content unrendered: %s", err.Error())
                return src, nil
        }
 
-       args := a.ParseArgs(ctx)
-       args = append(args, "-")
+       args, err := a.ParseArgs(ctx)
+       if err != nil {
+               return nil, err
+       }
+       args = append(args, "-") // read from stdin
 
-       a.Cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...")
+       a.Cfg.Logger.Infof("Rendering %s using Asciidoctor args %s ...", ctx.DocumentName, args)
 
        return internal.ExternallyRenderContent(a.Cfg, ctx, src, asciiDocBinaryName, args)
 }
 
-func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
-       cfg := a.Cfg.MarkupConfig().AsciidocExt
+func (a *AsciiDocConverter) ParseArgs(ctx converter.DocumentContext) ([]string, error) {
+       cfg := a.Cfg.MarkupConfig().AsciiDocExt
        args := []string{}
 
        args = a.AppendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend)
 
        for _, extension := range cfg.Extensions {
                if strings.LastIndexAny(extension, `\/.`) > -1 {
-                       a.Cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.")
+                       a.Cfg.Logger.Errorf(
+                               "The %q Asciidoctor extension is unsupported and ignored. Only installed Asciidoctor extensions are allowed.",
+                               extension,
+                       )
                        continue
                }
+
                args = append(args, "-r", extension)
+
+               if extension == asciiDocDiagramExtension {
+                       cacheDir := filepath.Clean(filepath.Join(a.Cfg.Conf.CacheDirMisc(), asciiDocDiagramExtension))
+                       args = append(args, "-a", asciiDocDiagramCacheDirKey+"="+cacheDir)
+                       args = append(args, "-a", asciiDocDiagramCacheImagesOptionKey)
+               }
        }
 
        for attributeKey, attributeValue := range cfg.Attributes {
                if asciidocext_config.DisallowedAttributes[attributeKey] {
-                       a.Cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.")
+                       a.Cfg.Logger.Errorf(
+                               "The %q Asciidoctor attribute is unsupported and ignored.",
+                               attributeKey,
+                       )
+                       continue
+               }
+
+               if attributeKey == asciiDocDiagramCacheImagesOptionKey {
+                       a.Cfg.Logger.Warnf(
+                               "The %q Asciidoctor attribute is fixed and cannot be modified. To disable caching of both image and metadata files, set markup.asciidocext.attributes.diagram-nocache-option to true in your site configuration.",
+                               attributeKey,
+                       )
+                       continue
+               }
+
+               if attributeKey == asciiDocDiagramCacheDirKey {
+                       a.Cfg.Logger.Warnf(
+                               "The %q Asciidoctor attribute is fixed and cannot be modified. To change the cache location, modify caches.misc.dir in your site configuration.",
+                               attributeKey,
+                       )
                        continue
                }
 
@@ -105,42 +167,63 @@ func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
        }
 
        if cfg.WorkingFolderCurrent {
-               contentDir := filepath.Dir(ctx.Filename)
-               destinationDir := a.Cfg.Conf.BaseConfig().PublishDir
+               page, ok := ctx.Document.(pageSubset)
+               if !ok {
+                       return nil, fmt.Errorf("expected pageSubset, got %T", ctx.Document)
+               }
 
-               if destinationDir == "" {
-                       a.Cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set")
+               // Derive the outdir document attribute from the relative permalink.
+               relPath := strings.TrimPrefix(page.RelPermalink(), a.Cfg.Conf.BaseURL().BasePathNoTrailingSlash)
+               relPath, err := url.PathUnescape(relPath)
+               if err != nil {
+                       return nil, err
                }
 
-               var outDir string
-               var err error
+               if a.Cfg.Conf.IsMultihost() {
+                       // In a multi-host configuration, neither absolute nor relative
+                       // permalinks include the language key; prepend it.
+                       language, ok := a.Cfg.Conf.Language().(*langs.Language)
+                       if !ok {
+                               return nil, fmt.Errorf("expected *langs.Language, got %T", a.Cfg.Conf.Language())
+                       }
+                       relPath = path.Join(language.Lang, relPath)
+               }
 
-               file := filepath.Base(ctx.Filename)
-               if a.Cfg.Conf.IsUglyURLs("") || file == "_index.adoc" || file == "index.adoc" {
-                       outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName)))
-               } else {
-                       postDir := ""
-                       page, ok := ctx.Document.(pageSubset)
-                       if ok {
-                               postDir = filepath.Base(page.RelPermalink())
+               if a.Cfg.Conf.IsUglyURLs(page.Section()) {
+                       if page.IsPage() {
+                               // Remove the extension.
+                               relPath = strings.TrimSuffix(relPath, path.Ext(relPath))
                        } else {
-                               a.Cfg.Logger.Errorln("unable to cast interface to pageSubset")
+                               // Remove the file name.
+                               relPath = path.Dir(relPath)
                        }
 
-                       outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir))
-               }
+                       // Set imagesoutdir and imagesdir attributes.
+                       imagesoutdir, err := filepath.Abs(filepath.Join(a.Cfg.Conf.BaseConfig().PublishDir, relPath))
+                       if err != nil {
+                               return nil, err
+                       }
+                       imagesdir := filepath.Base(imagesoutdir)
 
+                       if page.IsPage() {
+                               args = append(args, "-a", "imagesoutdir="+imagesoutdir, "-a", "imagesdir="+imagesdir)
+                       } else {
+                               args = append(args, "-a", "imagesoutdir="+imagesoutdir)
+                       }
+               }
+               // Prepend the publishDir.
+               outDir, err := filepath.Abs(filepath.Join(a.Cfg.Conf.BaseConfig().PublishDir, relPath))
                if err != nil {
-                       a.Cfg.Logger.Errorln("asciidoctor outDir: ", err)
+                       return nil, err
                }
 
-               args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir)
+               args = append(args, "--base-dir", filepath.Dir(ctx.Filename), "-a", "outdir="+outDir)
        }
 
        if cfg.NoHeaderOrFooter {
                args = append(args, "--no-header-footer")
        } else {
-               a.Cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering")
+               a.Cfg.Logger.Warnln("Asciidoctor parameter NoHeaderOrFooter is required for correct HTML rendering")
        }
 
        if cfg.SectionNumbers {
@@ -159,29 +242,71 @@ func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string {
 
        args = a.AppendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode)
 
-       return args
+       return args, nil
 }
 
-func (a *AsciidocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
+func (a *AsciiDocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string {
        if value != defaultValue {
                if allowedValues[value] {
                        args = append(args, option, value)
                } else {
-                       a.Cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.")
+                       a.Cfg.Logger.Errorf(
+                               "Unsupported Asciidoctor value %q for option %q was passed in and will be ignored.",
+                               value,
+                               option,
+                       )
                }
        }
        return args
 }
 
-const asciiDocBinaryName = "asciidoctor"
+// HasAsciiDoc reports whether the AsciiDoc converter is installed.
+func HasAsciiDoc() (bool, error) {
+       if !hexec.InPath(asciiDocBinaryName) {
+               return false, fmt.Errorf("the AsciiDoc converter (%s) is not installed", asciiDocBinaryName)
+       }
+       return true, nil
+}
+
+// CanRenderGoATDiagrams reports whether the AsciiDoc converter can render
+// GoAT diagrams. Only used in tests.
+func CanRenderGoATDiagrams() (bool, error) {
+       // Verify that the AsciiDoc converter is installed.
+       if ok, err := HasAsciiDoc(); !ok {
+               return false, err
+       }
+
+       // Verify that the RubyGems CLI is installed.
+       if !hexec.InPath(gemBinaryName) {
+               return false, fmt.Errorf("the RubyGems CLI (%s) is not installed", gemBinaryName)
+       }
+
+       // Verify that the required AsciiDoc converter extension is installed.
+       sc := security.DefaultConfig
+       sc.Exec.Allow = security.MustNewWhitelist(gemBinaryName)
+       ex := hexec.New(sc, "", loggers.NewDefault())
+
+       args := []any{"list", asciiDocDiagramExtension, "--installed"}
+       cmd, err := ex.New(gemBinaryName, args...)
+       if err != nil {
+               return false, err
+       }
+       err = cmd.Run()
+       if err != nil {
+               return false, fmt.Errorf("the %s gem is not installed", asciiDocDiagramExtension)
+       }
+
+       // Verify that the GoAT CLI is installed.
+       if !hexec.InPath(goatBinaryName) {
+               return false, fmt.Errorf("the GoAT CLI (%s) is not installed", goatBinaryName)
+       }
 
-func HasAsciiDoc() bool {
-       return hexec.InPath(asciiDocBinaryName)
+       return true, nil
 }
 
 // extractTOC extracts the toc from the given src html.
 // It returns the html without the TOC, and the TOC data
-func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
+func (a *AsciiDocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
        var buf bytes.Buffer
        buf.Write(src)
        node, err := html.Parse(&buf)
@@ -196,7 +321,7 @@ func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fra
        f = func(n *html.Node) bool {
                if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" {
                        toc = parseTOC(n)
-                       if !a.Cfg.MarkupConfig().AsciidocExt.PreserveTOC {
+                       if !a.Cfg.MarkupConfig().AsciiDocExt.PreserveTOC {
                                n.Parent.RemoveChild(n)
                        }
                        return true
index 50173cd6d539fa45df200f92e49f879fee014a6e..dfa66a52fd3940a72418019c804028cd6d7698cd 100644 (file)
@@ -37,8 +37,8 @@ type Config struct {
        // Configuration for the Goldmark markdown engine.
        Goldmark goldmark_config.Config
 
-       // Configuration for the Asciidoc external markdown engine.
-       AsciidocExt asciidocext_config.Config
+       // Configuration for the AsciiDoc external markdown engine.
+       AsciiDocExt asciidocext_config.Config
 }
 
 func (c *Config) Init() error {
@@ -118,5 +118,5 @@ var Default = Config{
        Highlight:       highlight.DefaultConfig,
 
        Goldmark:    goldmark_config.Default,
-       AsciidocExt: asciidocext_config.Default,
+       AsciiDocExt: asciidocext_config.Default,
 }
index 51ed8a49ec4f8910609afeab844c41be3c7ffc2a..57139b1e4835982d0a4204b2533cfb9fd27290ed 100644 (file)
@@ -48,8 +48,8 @@ func TestConfig(t *testing.T) {
                c.Assert(conf.Goldmark.Parser.Attribute.Title, qt.Equals, true)
                c.Assert(conf.Goldmark.Parser.Attribute.Block, qt.Equals, false)
 
-               c.Assert(conf.AsciidocExt.WorkingFolderCurrent, qt.Equals, true)
-               c.Assert(conf.AsciidocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
+               c.Assert(conf.AsciiDocExt.WorkingFolderCurrent, qt.Equals, true)
+               c.Assert(conf.AsciiDocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
        })
 
        // We changed the typographer extension config from a bool to a struct in 0.112.0.
index b8e010e21bf36193179035caa8219badbe7b5abe..8e2a2b6fcfec7e23ac8fa94e0bf72bd3945c13eb 100644 (file)
@@ -297,10 +297,10 @@ Summary Truncated: {{ .Truncated }}|
        )
 }
 
-func TestPageMarkupWithoutSummaryAsciidoc(t *testing.T) {
+func TestPageMarkupWithoutSummaryAsciiDoc(t *testing.T) {
        t.Parallel()
-       if !asciidocext.Supports() {
-               t.Skip("Skip asiidoc test as not supported")
+       if ok, err := asciidocext.Supports(); !ok {
+               t.Skip(err)
        }
 
        files := `