From: Joe Mooring Date: Fri, 24 Oct 2025 19:11:54 +0000 (-0700) Subject: markup/asciidocext: Improve Asciidoctor integration X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=3d21b0687b7d81115f866fae7a8ecb94d421dadc;p=brevno-suite%2Fhugo markup/asciidocext: Improve Asciidoctor integration 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 --- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb9db2eb8..b8670cb4d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go index a71ddb474..d52981174 100644 --- a/cache/filecache/filecache_config.go +++ b/cache/filecache/filecache_config.go @@ -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, diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index 9a7789efd..d27b1ba4f 100644 --- a/config/allconfig/configlanguage.go +++ b/config/allconfig/configlanguage.go @@ -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 } diff --git a/config/configProvider.go b/config/configProvider.go index 34e7ea365..4e6878c1f 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -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. diff --git a/docs/data/docs.yaml b/docs/data/docs.yaml index 9cd80c4f7..40b701d9b 100644 --- a/docs/data/docs.yaml +++ b/docs/data/docs.yaml @@ -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 - - - Search + - - Search - Search - - '{{ 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 "

Blockhead

\n" | safeHTML }}' -

Blockhead

@@ -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 `(.+?)` `
  • Foo
  • -
  • Bar
  • ` | print | safeHTML }}' + - - '{{ findRESubmatch `(.+?)` `
  • Foo
  • Bar
  • ` | print | safeHTML }}' - '[[Foo #foo Foo] [Bar #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 " | - safeHTML }}' + - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " | safeHTML }}' - Cathal Garvey & The Sunshine Band <cathal@foo.bar> - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " }}' - Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt; - - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " | - htmlUnescape | safeHTML }}' + - - '{{ htmlEscape "Cathal Garvey & The Sunshine Band " | htmlUnescape | safeHTML }}' - Cathal Garvey & The Sunshine Band + 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 & The Sunshine Band <cathal@foo.bar>" - | safeHTML }}' + - - '{{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | safeHTML }}' - Cathal Garvey & The Sunshine Band - - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" - | htmlUnescape | htmlUnescape | safeHTML }}' + - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape | safeHTML }}' - Cathal Garvey & The Sunshine Band - - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" - | htmlUnescape | htmlUnescape }}' + - - '{{ "Cathal Garvey &amp; The Sunshine Band &lt;cathal@foo.bar&gt;" | htmlUnescape | htmlUnescape }}' - Cathal Garvey & The Sunshine Band <cathal@foo.bar> - - - '{{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" - | htmlEscape | safeHTML }}' + - - '{{ htmlUnescape "Cathal Garvey & The Sunshine Band <cathal@foo.bar>" | htmlEscape | safeHTML }}' - Cathal Garvey & The Sunshine Band <cathal@foo.bar> 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: diff --git a/htesting/test_helpers.go b/htesting/test_helpers.go index 2043b1a4b..74e349424 100644 --- a/htesting/test_helpers.go +++ b/htesting/test_helpers.go @@ -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") != "" } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 3f6aa93c4..0f7ac65e6 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -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, "

    Lorem ipsum 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.

    \n\n

    Additional text.

    \n\n

    Further text.

    \n"), ext) checkPageSummary(t, p, normalizeExpected(ext, "

    Lorem ipsum 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.

    Additional text.

    "), ext) @@ -604,7 +604,7 @@ func TestPageWithSummaryParameter(t *testing.T) { p := pages[0] checkPageTitle(t, p, "SimpleWithSummaryParameter") checkPageContent(t, p, normalizeExpected(ext, "

    Some text.

    \n\n

    Some more text.

    \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 link"), ext) } diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go index 556fb84a9..b0c611c50 100644 --- a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go +++ b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go @@ -132,7 +132,7 @@ docs/p1/sub/mymixcasetext2.txt b.AssertFileContent("public/docs/p3/index.html", "Hello World Default") } -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, automation is essential", "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" diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go index c4d7f754f..c79bc5c57 100644 --- a/hugolib/rebuild_test.go +++ b/hugolib/rebuild_test.go @@ -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 -- diff --git a/hugolib/securitypolicies_test.go b/hugolib/securitypolicies_test.go index 57bf302a4..a7639be8b 100644 --- a/hugolib/securitypolicies_test.go +++ b/hugolib/securitypolicies_test.go @@ -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 index 000000000..55c898592 --- /dev/null +++ b/markup/asciidocext/asciidoc_integration_test.go @@ -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) + } +} diff --git a/markup/asciidocext/convert.go b/markup/asciidocext/convert.go index ecf3eb9ac..e8d59a521 100644 --- a/markup/asciidocext/convert.go +++ b/markup/asciidocext/convert.go @@ -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 } diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go index bfcd4ae8f..6166db069 100644 --- a/markup/asciidocext/convert_test.go +++ b/markup/asciidocext/convert_test.go @@ -11,14 +11,14 @@ // 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 := ` diff --git a/markup/asciidocext/internal/converter.go b/markup/asciidocext/internal/converter.go index 47e406641..594f6fbbf 100644 --- a/markup/asciidocext/internal/converter.go +++ b/markup/asciidocext/internal/converter.go @@ -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 diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go index 50173cd6d..dfa66a52f 100644 --- a/markup/markup_config/config.go +++ b/markup/markup_config/config.go @@ -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, } diff --git a/markup/markup_config/config_test.go b/markup/markup_config/config_test.go index 51ed8a49e..57139b1e4 100644 --- a/markup/markup_config/config_test.go +++ b/markup/markup_config/config_test.go @@ -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. diff --git a/resources/page/page_markup_integration_test.go b/resources/page/page_markup_integration_test.go index b8e010e21..8e2a2b6fc 100644 --- a/resources/page/page_markup_integration_test.go +++ b/resources/page/page_markup_integration_test.go @@ -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 := `