mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-27 16:58:53 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10ddc2ef92 | |||
| c406fd3a0e | |||
| 286821e360 | |||
| 79f15be5b0 | |||
| 5c2e014a51 | |||
| cc44583cc3 | |||
| 4da672af88 | |||
| f1886f8c37 | |||
| 5bec50838c | |||
| f650e4d751 | |||
| c934a45069 | |||
| 91b02091a5 | |||
| 8966424e0e | |||
| 72510969a6 | |||
| 70c5e485bb | |||
| a78b17d7f1 | |||
| a481942532 | |||
| 0f921ace6f | |||
| 6019953769 | |||
| 92e86702ea | |||
| 6c9ea022a9 | |||
| 12d3469dd1 | |||
| bf7ee8a91a | |||
| d912491f2d | |||
| 857374e693 | |||
| c27639b9a0 | |||
| ceb486f98c | |||
| 5afc89f2bf | |||
| c1eac616d5 | |||
| 3c8256a137 | |||
| 19d76ae96b | |||
| 11ecea6106 | |||
| da98724bc8 | |||
| 4d470bb732 | |||
| 0ff8e13c1c | |||
| ffd37d4f75 | |||
| b4b65245b2 | |||
| 87886f40d1 | |||
| 67caf50698 | |||
| 7917961d59 | |||
| 80ecb95895 | |||
| 58e09cc6c7 | |||
| 635cc346ce | |||
| 12646750aa | |||
| 79639c981c | |||
| 9b313cec1b | |||
| b74b8d6478 | |||
| 793e38f5ce | |||
| 7f698c8934 | |||
| fa0e16f4c7 | |||
| 12e4c4d5dc | |||
| bac03f4076 | |||
| cc14c6a52c | |||
| e3308a0bbc | |||
| 019299b0b0 | |||
| 23ed087c4e | |||
| a018259bcf | |||
| ad5e04daa4 | |||
| 5b4bfc2db7 | |||
| 92f55f1121 |
@@ -104,3 +104,10 @@ jobs:
|
||||
- name: Build tags
|
||||
run: |
|
||||
go install -tags extended,nodeploy
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
name: Build for dragonfly
|
||||
run: |
|
||||
go install
|
||||
env:
|
||||
GOARCH: amd64
|
||||
GOOS: dragonfly
|
||||
|
||||
@@ -1,243 +1,290 @@
|
||||
[bep]: https://github.com/spf13
|
||||
[bugs]: https://github.com/gohugoio/hugo/issues?q=is%3Aopen+is%3Aissue+label%3ABug
|
||||
[contributing]: CONTRIBUTING.md
|
||||
[create a proposal]: https://github.com/gohugoio/hugo/issues/new?labels=Proposal%2C+NeedsTriage&template=feature_request.md
|
||||
[documentation repository]: https://github.com/gohugoio/hugoDocs
|
||||
[documentation]: https://gohugo.io/documentation
|
||||
[dragonfly bsd, freebsd, netbsd, and openbsd]: https://gohugo.io/installation/bsd
|
||||
[forum]: https://discourse.gohugo.io
|
||||
[friends]: https://github.com/gohugoio/hugo/graphs/contributors
|
||||
[go]: https://go.dev/
|
||||
[hugo modules]: https://gohugo.io/hugo-modules/
|
||||
[installation]: https://gohugo.io/installation
|
||||
[issue queue]: https://github.com/gohugoio/hugo/issues
|
||||
[linux]: https://gohugo.io/installation/linux
|
||||
[macos]: https://gohugo.io/installation/macos
|
||||
[prebuilt binary]: https://github.com/gohugoio/hugo/releases/latest
|
||||
[requesting help]: https://discourse.gohugo.io/t/requesting-help/9132
|
||||
[spf13]: https://github.com/spf13
|
||||
[static site generator]: https://en.wikipedia.org/wiki/Static_site_generator
|
||||
[support]: https://discourse.gohugo.io
|
||||
[themes]: https://themes.gohugo.io/
|
||||
[twitter]: https://twitter.com/gohugoio
|
||||
[website]: https://gohugo.io
|
||||
[windows]: https://gohugo.io/installation/windows
|
||||
|
||||
<a href="https://gohugo.io/"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/static/images/hugo-logo-wide.svg?sanitize=true" alt="Hugo" width="565"></a>
|
||||
|
||||
A Fast and Flexible Static Site Generator built with love by [bep](https://github.com/bep), [spf13](https://spf13.com/) and [friends](https://github.com/gohugoio/hugo/graphs/contributors) in [Go](https://go.dev/).
|
||||
A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go].
|
||||
|
||||
[Website](https://gohugo.io) |
|
||||
[Forum](https://discourse.gohugo.io) |
|
||||
[Documentation](https://gohugo.io/getting-started/) |
|
||||
[Installation Guide](https://gohugo.io/getting-started/installing/) |
|
||||
[Contribution Guide](CONTRIBUTING.md) |
|
||||
[Twitter](https://twitter.com/gohugoio)
|
||||
---
|
||||
|
||||
[](https://godoc.org/github.com/gohugoio/hugo)
|
||||
[](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest)
|
||||
[](https://goreportcard.com/report/github.com/gohugoio/hugo)
|
||||
|
||||
* [Overview](#overview)
|
||||
* [Banner Sponsors](#banner-sponsors)
|
||||
* [Supported Architectures](#supported-architectures)
|
||||
* [Choose How to Install](#choose-how-to-install)
|
||||
* [Install Hugo as Your Site Generator (Binary Install)](#install-hugo-as-your-site-generator-binary-install)
|
||||
* [Build and Install the Binary from Source (Using the Go toolchain)](#build-and-install-the-binary-from-source-using-the-go-toolchain)
|
||||
* [The Hugo Documentation](#the-hugo-documentation)
|
||||
* [Contributing to Hugo](#contributing-code-to-hugo)
|
||||
* [Dependencies](#dependencies)
|
||||
[Website] | [Installation] | [Documentation] | [Support] | [Contributing] | [Twitter]
|
||||
|
||||
## Overview
|
||||
|
||||
Hugo is a static HTML and CSS website generator written in [Go](https://go.dev/).
|
||||
It is optimized for speed, ease of use, and configurability.
|
||||
Hugo takes a directory with content and templates and renders them into a full HTML website.
|
||||
Hugo is a [static site generator] written in [Go], optimized for speed and designed for flexibility. With its advanced templating system and fast asset pipelines, Hugo renders a complete site in seconds, often less.
|
||||
|
||||
Hugo relies on Markdown files with front matter for metadata, and you can run Hugo from any directory.
|
||||
This works well for shared hosts and other systems where you don’t have a privileged account.
|
||||
Due to its flexible framework, multilingual support, and powerful taxonomy system, Hugo is widely used to create:
|
||||
|
||||
Hugo renders a typical website of moderate size in a fraction of a second.
|
||||
A good rule of thumb is that each piece of content renders in around 1 millisecond.
|
||||
- Corporate, government, nonprofit, education, news, event, and project sites
|
||||
- Documentation sites
|
||||
- Image portfolios
|
||||
- Landing pages
|
||||
- Business, professional, and personal blogs
|
||||
- Resumes and CVs
|
||||
|
||||
Hugo is designed to work well for any kind of website including blogs, tumbles, and docs.
|
||||
Use Hugo's embedded web server during development to instantly see changes to content, structure, behavior, and presentation. Then deploy the site to your host, or push changes to your Git provider for automated builds and deployment.
|
||||
|
||||
Hugo's fast asset pipelines include:
|
||||
|
||||
- CSS bundling – transpilation (Sass), tree shaking, minification, source maps, SRI hashing, and PostCSS integration
|
||||
- JavaScript bundling – transpilation (TypeScript, JSX), tree shaking, minification, source maps, and SRI hashing
|
||||
- Image processing – convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
|
||||
|
||||
And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories.
|
||||
|
||||
## Sponsors
|
||||
|
||||
## Banner Sponsors
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/cloudcannon-blue.svg" width="220" alt="CloudCannon"></a>
|
||||
<p> </p>
|
||||
|
||||
## Supported Architectures
|
||||
## Installation
|
||||
|
||||
Currently, we provide pre-built Hugo binaries for Windows, Linux, FreeBSD, NetBSD, DragonFly BSD, OpenBSD, macOS (Darwin), and [Android](https://gist.github.com/bep/a0d8a26cf6b4f8bc992729b8e50b480b) for x64, i386 and ARM architectures.
|
||||
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
|
||||
|
||||
Hugo may also be compiled from source wherever the Go compiler tool chain can run, e.g. for other operating systems including Plan 9 and Solaris.
|
||||
- [macOS]
|
||||
- [Linux]
|
||||
- [Windows]
|
||||
- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD]
|
||||
|
||||
**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).**
|
||||
## Build from source
|
||||
|
||||
## Choose How to Install
|
||||
Hugo is available in two editions: standard and extended. With the extended edition you can:
|
||||
|
||||
If you want to use Hugo as your site generator, simply install the Hugo binaries.
|
||||
- Encode to the WebP format when processing images. You can decode WebP images with either edition.
|
||||
- Transpile Sass to CSS using the embedded LibSass transpiler. The extended edition is not required to use the Dart Sass transpiler.
|
||||
|
||||
To contribute to the Hugo source code or documentation, you should [fork the Hugo GitHub project](https://github.com/gohugoio/hugo#fork-destination-box) and clone it to your local machine.
|
||||
Prerequisites to build Hugo from source:
|
||||
|
||||
Finally, you can install the Hugo source code with `go`, build the binaries yourself, and run Hugo that way.
|
||||
Building the binaries is an easy task for an experienced `go` getter.
|
||||
- Standard edition: Go 1.19 or later
|
||||
- Extended edition: Go 1.19 or later, and GCC
|
||||
|
||||
### Install Hugo as Your Site Generator (Binary Install)
|
||||
Build the standard edition:
|
||||
|
||||
Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/).
|
||||
|
||||
### Build and Install the Binary from Source (Using the Go toolchain)
|
||||
|
||||
#### Prerequisite Tools
|
||||
|
||||
* [Go (we test it with the last 2 major versions; but note that Hugo 0.95.0 only builds with >= Go 1.18.)](https://golang.org/dl/)
|
||||
|
||||
#### Fetch from GitHub
|
||||
|
||||
To fetch, build and install from the Github source:
|
||||
|
||||
```bash
|
||||
```text
|
||||
go install github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
If you want to compile with Sass/SCSS support use `-tags extended` and make sure `CGO_ENABLED=1` is set in your go environment. If you don't want to have CGO enabled, you may use the following command to temporarily enable CGO only for hugo compilation:
|
||||
Build the extended edition:
|
||||
|
||||
```bash
|
||||
```text
|
||||
CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
|
||||
```
|
||||
|
||||
## The Hugo Documentation
|
||||
## Documentation
|
||||
|
||||
The Hugo documentation now lives in its own repository, see https://github.com/gohugoio/hugoDocs. But we do keep a version of that documentation as a `git subtree` in this repository. To build the sub folder `/docs` as a Hugo site, you need to clone this repo:
|
||||
Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:gohugoio/hugo.git
|
||||
```
|
||||
## Contributing code to Hugo
|
||||
Please submit documentation issues and pull requests to the [documentation repository].
|
||||
|
||||
## Support
|
||||
|
||||
Please **do not use the issue queue** for questions or troubleshooting. Unless you are certain that your issue is a software defect, use the [forum].
|
||||
|
||||
Hugo’s [forum] is an active community of users and developers who answer questions, share knowledge, and provide examples. A quick search of over 20,000 topics will often answer your question. Please be sure to read about [requesting help] before asking your first question.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can contribute to the Hugo project by:
|
||||
|
||||
- Answering questions on the [forum]
|
||||
- Improving the [documentation]
|
||||
- Monitoring the [issue queue]
|
||||
- Creating or improving [themes]
|
||||
- Squashing [bugs]
|
||||
|
||||
Please submit documentation issues and pull requests to the [documentation repository].
|
||||
|
||||
If you have an idea for an enhancement or new feature, create a new topic on the [forum] in the "Feature" category. This will help you to:
|
||||
|
||||
- Determine if the capability already exists
|
||||
- Measure interest
|
||||
- Refine the concept
|
||||
|
||||
If there is sufficient interest, [create a proposal]. Do not submit a pull request until the project lead accepts the proposal.
|
||||
|
||||
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
|
||||
|
||||
We welcome contributions to Hugo of any kind including documentation, themes,
|
||||
organization, tutorials, blog posts, bug reports, issues, feature requests,
|
||||
feature implementations, pull requests, answering questions on the forum,
|
||||
helping to manage issues, etc.
|
||||
|
||||
The Hugo community and maintainers are [very active](https://github.com/gohugoio/hugo/pulse/monthly) and helpful, and the project benefits greatly from this activity.
|
||||
|
||||
## Asking Support Questions
|
||||
|
||||
We have an active [discussion forum](https://discourse.gohugo.io) where users and developers can ask questions.
|
||||
Please don't use the GitHub issue tracker to ask questions.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
If you believe you have found a defect in Hugo or its documentation, use
|
||||
the GitHub issue tracker to report the problem to the Hugo maintainers.
|
||||
If you're not sure if it's a bug or not, start by asking in the [discussion forum](https://discourse.gohugo.io).
|
||||
When reporting the issue, please provide the version of Hugo in use (`hugo version`).
|
||||
|
||||
## Dependencies
|
||||
|
||||
Hugo stands on the shoulder of many great open source libraries.
|
||||
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
|
||||
|
||||
If you run `hugo env -v` you will get a complete and up to date list.
|
||||
<details>
|
||||
<summary>See current dependencies</summary>
|
||||
|
||||
In Hugo 0.111.2 that list is, in lexical order:
|
||||
|
||||
```
|
||||
cloud.google.com/go/compute="v1.6.1"
|
||||
cloud.google.com/go/iam="v0.3.0"
|
||||
cloud.google.com/go/storage="v1.22.0"
|
||||
cloud.google.com/go="v0.101.0"
|
||||
github.com/Azure/azure-pipeline-go="v0.2.3"
|
||||
github.com/Azure/azure-storage-blob-go="v0.14.0"
|
||||
github.com/Azure/go-autorest/autorest/adal="v0.9.15"
|
||||
github.com/Azure/go-autorest/autorest/date="v0.3.0"
|
||||
github.com/Azure/go-autorest/autorest="v0.11.20"
|
||||
github.com/Azure/go-autorest/logger="v0.2.1"
|
||||
github.com/Azure/go-autorest/tracing="v0.6.0"
|
||||
```text
|
||||
cloud.google.com/go/compute/metadata="v0.2.3"
|
||||
cloud.google.com/go/iam="v1.1.0"
|
||||
cloud.google.com/go/storage="v1.30.1"
|
||||
cloud.google.com/go="v0.110.2"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore="v1.6.1"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal="v1.3.0"
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob="v1.0.0"
|
||||
github.com/Azure/go-autorest/autorest/to="v0.4.0"
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go="v1.0.0"
|
||||
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
|
||||
github.com/PuerkitoBio/purell="v1.1.1"
|
||||
github.com/PuerkitoBio/urlesc="v0.0.0-20170810143723-de5bf2ad4578"
|
||||
github.com/alecthomas/chroma/v2="v2.5.0"
|
||||
github.com/alecthomas/chroma/v2="v2.7.0"
|
||||
github.com/armon/go-radix="v1.0.0"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.5.0"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.2.2"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.3.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.4.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.7.0"
|
||||
github.com/aws/aws-sdk-go-v2="v1.9.0"
|
||||
github.com/aws/aws-sdk-go="v1.43.5"
|
||||
github.com/aws/smithy-go="v1.8.0"
|
||||
github.com/bep/clock="v0.3.0"
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream="v1.4.10"
|
||||
github.com/aws/aws-sdk-go-v2/config="v1.18.27"
|
||||
github.com/aws/aws-sdk-go-v2/credentials="v1.13.26"
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds="v1.13.4"
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager="v1.11.70"
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources="v1.1.34"
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2="v2.4.28"
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini="v1.3.35"
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a="v1.0.26"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding="v1.9.11"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum="v1.1.29"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url="v1.9.28"
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared="v1.14.3"
|
||||
github.com/aws/aws-sdk-go-v2/service/s3="v1.35.0"
|
||||
github.com/aws/aws-sdk-go-v2/service/sso="v1.12.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc="v1.14.12"
|
||||
github.com/aws/aws-sdk-go-v2/service/sts="v1.19.2"
|
||||
github.com/aws/aws-sdk-go-v2="v1.18.1"
|
||||
github.com/aws/aws-sdk-go="v1.44.284"
|
||||
github.com/aws/smithy-go="v1.13.5"
|
||||
github.com/bep/clocks="v0.5.0"
|
||||
github.com/bep/debounce="v1.2.0"
|
||||
github.com/bep/gitmap="v1.1.2"
|
||||
github.com/bep/goat="v0.5.0"
|
||||
github.com/bep/godartsass="v0.16.0"
|
||||
github.com/bep/golibsass="v1.1.0"
|
||||
github.com/bep/godartsass/v2="v2.0.0"
|
||||
github.com/bep/godartsass="v1.2.0"
|
||||
github.com/bep/golibsass="v1.1.1"
|
||||
github.com/bep/gowebp="v0.2.0"
|
||||
github.com/bep/lazycache="v0.2.0"
|
||||
github.com/bep/logg="v0.2.0"
|
||||
github.com/bep/mclib="v1.20400.20402"
|
||||
github.com/bep/overlayfs="v0.6.0"
|
||||
github.com/bep/simplecobra="v0.3.2"
|
||||
github.com/bep/tmc="v0.5.1"
|
||||
github.com/clbanning/mxj/v2="v2.5.7"
|
||||
github.com/cli/safeexec="v1.0.0"
|
||||
github.com/cli/safeexec="v1.0.1"
|
||||
github.com/cpuguy83/go-md2man/v2="v2.0.2"
|
||||
github.com/disintegration/gift="v1.2.1"
|
||||
github.com/dlclark/regexp2="v1.7.0"
|
||||
github.com/dustin/go-humanize="v1.0.0"
|
||||
github.com/evanw/esbuild="v0.17.0"
|
||||
github.com/frankban/quicktest="v1.14.4"
|
||||
github.com/dlclark/regexp2="v1.10.0"
|
||||
github.com/dustin/go-humanize="v1.0.1"
|
||||
github.com/evanw/esbuild="v0.18.5"
|
||||
github.com/fatih/color="v1.15.0"
|
||||
github.com/frankban/quicktest="v1.14.5"
|
||||
github.com/fsnotify/fsnotify="v1.6.0"
|
||||
github.com/getkin/kin-openapi="v0.110.0"
|
||||
github.com/getkin/kin-openapi="v0.118.0"
|
||||
github.com/ghodss/yaml="v1.0.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.5"
|
||||
github.com/go-openapi/swag="v0.19.5"
|
||||
github.com/gobuffalo/flect="v0.3.0"
|
||||
github.com/go-openapi/jsonpointer="v0.19.6"
|
||||
github.com/go-openapi/swag="v0.22.3"
|
||||
github.com/gobuffalo/flect="v1.0.2"
|
||||
github.com/gobwas/glob="v0.2.3"
|
||||
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013"
|
||||
github.com/gohugoio/locales="v0.14.0"
|
||||
github.com/gohugoio/localescompressed="v1.0.1"
|
||||
github.com/golang-jwt/jwt/v4="v4.0.0"
|
||||
github.com/golang-jwt/jwt/v4="v4.5.0"
|
||||
github.com/golang/groupcache="v0.0.0-20210331224755-41bb18bfe9da"
|
||||
github.com/golang/protobuf="v1.5.2"
|
||||
github.com/golang/protobuf="v1.5.3"
|
||||
github.com/google/go-cmp="v0.5.9"
|
||||
github.com/google/s2a-go="v0.1.4"
|
||||
github.com/google/uuid="v1.3.0"
|
||||
github.com/google/wire="v0.5.0"
|
||||
github.com/googleapis/gax-go/v2="v2.3.0"
|
||||
github.com/googleapis/go-type-adapters="v1.0.0"
|
||||
github.com/googleapis/enterprise-certificate-proxy="v0.2.5"
|
||||
github.com/googleapis/gax-go/v2="v2.11.0"
|
||||
github.com/gorilla/websocket="v1.5.0"
|
||||
github.com/hairyhenderson/go-codeowners="v0.2.3-0.20201026200250-cdc7c0759690"
|
||||
github.com/hairyhenderson/go-codeowners="v0.3.0"
|
||||
github.com/hashicorp/golang-lru/v2="v2.0.1"
|
||||
github.com/invopop/yaml="v0.1.0"
|
||||
github.com/jdkato/prose="v1.2.1"
|
||||
github.com/jmespath/go-jmespath="v0.4.0"
|
||||
github.com/josharian/intern="v1.0.0"
|
||||
github.com/kr/pretty="v0.3.1"
|
||||
github.com/kr/text="v0.2.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.11"
|
||||
github.com/mailru/easyjson="v0.0.0-20190626092158-b2ccc519800e"
|
||||
github.com/kylelemons/godebug="v1.1.0"
|
||||
github.com/kyokomi/emoji/v2="v2.2.12"
|
||||
github.com/mailru/easyjson="v0.7.7"
|
||||
github.com/marekm4/color-extractor="v1.2.0"
|
||||
github.com/mattn/go-ieproxy="v0.0.1"
|
||||
github.com/mattn/go-isatty="v0.0.17"
|
||||
github.com/mattn/go-colorable="v0.1.13"
|
||||
github.com/mattn/go-isatty="v0.0.19"
|
||||
github.com/mattn/go-runewidth="v0.0.9"
|
||||
github.com/mitchellh/hashstructure="v1.1.0"
|
||||
github.com/mitchellh/mapstructure="v1.5.0"
|
||||
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
|
||||
github.com/muesli/smartcrop="v0.3.0"
|
||||
github.com/niklasfasching/go-org="v1.6.5"
|
||||
github.com/niklasfasching/go-org="v1.7.0"
|
||||
github.com/olekukonko/tablewriter="v0.0.5"
|
||||
github.com/pelletier/go-toml/v2="v2.0.6"
|
||||
github.com/rogpeppe/go-internal="v1.9.0"
|
||||
github.com/pelletier/go-toml/v2="v2.0.8"
|
||||
github.com/perimeterx/marshmallow="v1.1.4"
|
||||
github.com/pkg/browser="v0.0.0-20210911075715-681adbf594b8"
|
||||
github.com/pkg/errors="v0.9.1"
|
||||
github.com/rogpeppe/go-internal="v1.10.1-0.20230508101108-a4f6fabd84c5"
|
||||
github.com/russross/blackfriday/v2="v2.1.0"
|
||||
github.com/rwcarlsen/goexif="v0.0.0-20190401172101-9e8deecbddbd"
|
||||
github.com/sanity-io/litter="v1.5.5"
|
||||
github.com/sass/dart-sass/compiler="1.63.6"
|
||||
github.com/sass/dart-sass/implementation="1.63.6"
|
||||
github.com/sass/dart-sass/protocol="2.1.0"
|
||||
github.com/sass/libsass="3.6.5"
|
||||
github.com/spf13/afero="v1.9.3"
|
||||
github.com/spf13/cast="v1.5.0"
|
||||
github.com/spf13/cobra="v1.6.1"
|
||||
github.com/spf13/afero="v1.9.5"
|
||||
github.com/spf13/cast="v1.5.1"
|
||||
github.com/spf13/cobra="v1.7.0"
|
||||
github.com/spf13/fsync="v0.9.0"
|
||||
github.com/spf13/jwalterweatherman="v1.1.0"
|
||||
github.com/spf13/pflag="v1.0.5"
|
||||
github.com/tdewolff/minify/v2="v2.12.4"
|
||||
github.com/tdewolff/parse/v2="v2.6.5"
|
||||
github.com/tdewolff/minify/v2="v2.12.7"
|
||||
github.com/tdewolff/parse/v2="v2.6.6"
|
||||
github.com/webmproject/libwebp="v1.2.4"
|
||||
github.com/yuin/goldmark="v1.5.4"
|
||||
go.opencensus.io="v0.24.0"
|
||||
go.uber.org/atomic="v1.10.0"
|
||||
gocloud.dev="v0.24.0"
|
||||
golang.org/x/crypto="v0.3.0"
|
||||
golang.org/x/exp="v0.0.0-20221031165847-c99f073a8326"
|
||||
golang.org/x/image="v0.5.0"
|
||||
golang.org/x/net="v0.7.0"
|
||||
golang.org/x/oauth2="v0.2.0"
|
||||
golang.org/x/sync="v0.1.0"
|
||||
golang.org/x/sys="v0.5.0"
|
||||
golang.org/x/text="v0.7.0"
|
||||
golang.org/x/tools="v0.4.0"
|
||||
go.uber.org/atomic="v1.11.0"
|
||||
go.uber.org/automaxprocs="v1.5.2"
|
||||
gocloud.dev="v0.30.0"
|
||||
golang.org/x/crypto="v0.10.0"
|
||||
golang.org/x/exp="v0.0.0-20230321023759-10a507213a29"
|
||||
golang.org/x/image="v0.8.0"
|
||||
golang.org/x/mod="v0.10.0"
|
||||
golang.org/x/net="v0.11.0"
|
||||
golang.org/x/oauth2="v0.9.0"
|
||||
golang.org/x/sync="v0.3.0"
|
||||
golang.org/x/sys="v0.9.0"
|
||||
golang.org/x/text="v0.10.0"
|
||||
golang.org/x/tools="v0.9.3"
|
||||
golang.org/x/xerrors="v0.0.0-20220907171357-04be3eba64a2"
|
||||
google.golang.org/api="v0.76.0"
|
||||
google.golang.org/genproto="v0.0.0-20220426171045-31bebdecfb46"
|
||||
google.golang.org/grpc="v1.46.0"
|
||||
google.golang.org/protobuf="v1.28.1"
|
||||
google.golang.org/api="v0.128.0"
|
||||
google.golang.org/appengine="v1.6.7"
|
||||
google.golang.org/genproto/googleapis/api="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto/googleapis/rpc="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/genproto="v0.0.0-20230530153820-e85fd2cbaebc"
|
||||
google.golang.org/grpc="v1.56.0"
|
||||
google.golang.org/protobuf="v1.30.0"
|
||||
gopkg.in/yaml.v2="v2.4.0"
|
||||
gopkg.in/yaml.v3="v3.0.1"
|
||||
software.sslmate.com/src/go-pkcs12="v0.2.0"
|
||||
```
|
||||
</details>
|
||||
|
||||
+10
-12
@@ -30,7 +30,7 @@ import (
|
||||
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/clocks"
|
||||
"github.com/bep/lazycache"
|
||||
"github.com/bep/logg"
|
||||
"github.com/bep/overlayfs"
|
||||
@@ -110,12 +110,10 @@ type rootCommand struct {
|
||||
environment string
|
||||
|
||||
// Common build flags.
|
||||
baseURL string
|
||||
gc bool
|
||||
poll string
|
||||
forceSyncStatic bool
|
||||
printPathWarnings bool
|
||||
printUnusedTemplates bool
|
||||
baseURL string
|
||||
gc bool
|
||||
poll string
|
||||
forceSyncStatic bool
|
||||
|
||||
// Profile flags (for debugging of performance problems)
|
||||
cpuprofile string
|
||||
@@ -175,7 +173,7 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo
|
||||
|
||||
if !configs.Base.C.Clock.IsZero() {
|
||||
// TODO(bep) find a better place for this.
|
||||
htime.Clock = clock.Start(configs.Base.C.Clock)
|
||||
htime.Clock = clocks.Start(configs.Base.C.Clock)
|
||||
}
|
||||
|
||||
return &commonConfig{
|
||||
@@ -285,10 +283,10 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo
|
||||
|
||||
if !base.C.Clock.IsZero() {
|
||||
// TODO(bep) find a better place for this.
|
||||
htime.Clock = clock.Start(configs.Base.C.Clock)
|
||||
htime.Clock = clocks.Start(configs.Base.C.Clock)
|
||||
}
|
||||
|
||||
if base.LogPathWarnings {
|
||||
if base.PrintPathWarnings {
|
||||
// Note that we only care about the "dynamic creates" here,
|
||||
// so skip the static fs.
|
||||
fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir)
|
||||
@@ -544,8 +542,8 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
|
||||
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
|
||||
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
|
||||
cmd.Flags().BoolVarP(&r.printPathWarnings, "printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolVarP(&r.printUnusedTemplates, "printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
|
||||
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
|
||||
cmd.Flags().StringVarP(&r.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
|
||||
cmd.Flags().StringVarP(&r.memprofile, "profile-mem", "", "", "write memory profile to `file`")
|
||||
cmd.Flags().BoolVarP(&r.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
|
||||
|
||||
@@ -110,6 +110,7 @@ See convert's subcommands toJSON, toTOML and toYAML for more information.`
|
||||
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
|
||||
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
|
||||
"github.com/bep/simplecobra"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -243,6 +243,8 @@ func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [
|
||||
func (c *genCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
cmd := cd.CobraCommand
|
||||
cmd.Short = "A collection of several useful generators."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+3
-5
@@ -79,11 +79,9 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
|
||||
|
||||
// Flags with a different name in the config.
|
||||
keyMap := map[string]string{
|
||||
"minify": "minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"printI18nWarnings": "logI18nWarnings",
|
||||
"printPathWarnings": "logPathWarnings",
|
||||
"editor": "newContentEditor",
|
||||
"minify": "minifyOutput",
|
||||
"destination": "publishDir",
|
||||
"editor": "newContentEditor",
|
||||
}
|
||||
|
||||
// Flags that we for some reason don't want to expose in the site config.
|
||||
|
||||
+2
-23
@@ -43,9 +43,7 @@ import (
|
||||
"github.com/gohugoio/hugo/hugolib/filesystems"
|
||||
"github.com/gohugoio/hugo/livereload"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/gohugoio/hugo/watcher"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/fsync"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/sync/semaphore"
|
||||
@@ -419,25 +417,6 @@ func (c *hugoBuilder) build() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.r.printPathWarnings {
|
||||
hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool {
|
||||
if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
|
||||
dupes := dfs.ReportDuplicates()
|
||||
if dupes != "" {
|
||||
c.r.logger.Warnln("Duplicate target paths:", dupes)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if c.r.printUnusedTemplates {
|
||||
unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
|
||||
for _, unusedTemplate := range unusedTemplates {
|
||||
c.r.logger.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
h.PrintProcessingStats(os.Stdout)
|
||||
c.r.Println()
|
||||
}
|
||||
@@ -1063,8 +1042,8 @@ func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error {
|
||||
if c.fastRenderMode {
|
||||
c.withConf(func(conf *commonConfig) {
|
||||
// Make sure we always render the home pages
|
||||
for _, l := range conf.configs.Languages {
|
||||
langPath := h.GetLangSubDir(l.Lang)
|
||||
for _, l := range conf.configs.ConfigLangs() {
|
||||
langPath := l.LanguagePrefix()
|
||||
if langPath != "" {
|
||||
langPath = langPath + "/"
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
|
||||
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ func (c *listCommand) Init(cd *simplecobra.Commandeer) error {
|
||||
|
||||
List requires a subcommand, e.g. hugo list drafts`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -224,7 +224,7 @@ Run "go help get" for more information. All flags available for "go get" is also
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
// We currently just pass on the flags we get to Go and
|
||||
// need to do the flag handling manually.
|
||||
if len(args) == 1 && args[0] == "-h" {
|
||||
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
|
||||
return errHelp
|
||||
}
|
||||
|
||||
|
||||
+22
-15
@@ -47,16 +47,16 @@ func newNewCommand() *newCommand {
|
||||
use: "content [path]",
|
||||
short: "Create new content for your site",
|
||||
long: `Create a new content file and automatically set the date and title.
|
||||
It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
It will guess which kind of file to create based on the path provided.
|
||||
|
||||
You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("path needs to be provided")
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
@@ -82,7 +82,7 @@ The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("path needs to be provided")
|
||||
return newUserError("path needs to be provided")
|
||||
}
|
||||
createpath, err := filepath.Abs(filepath.Clean(args[0]))
|
||||
if err != nil {
|
||||
@@ -150,16 +150,21 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
},
|
||||
withc: func(cmd *cobra.Command, r *rootCommand) {
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "init inside non-empty directory")
|
||||
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
|
||||
},
|
||||
},
|
||||
&simpleCommand{
|
||||
name: "theme",
|
||||
use: "theme [path]",
|
||||
short: "Create a new site (skeleton)",
|
||||
long: `Create a new site in the provided directory.
|
||||
The new site will have the correct structure, but no content or theme yet.
|
||||
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
|
||||
use: "theme [name]",
|
||||
short: "Create a new theme (skeleton)",
|
||||
long: `Create a new theme (skeleton) called [name] in ./themes.
|
||||
New theme is a skeleton. Please add content to the touched files. Add your
|
||||
name to the copyright line in the license and adjust the theme.toml file
|
||||
according to your needs.`,
|
||||
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return newUserError("theme name needs to be provided")
|
||||
}
|
||||
h, err := r.Hugo(flagsToCfg(cd, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -284,6 +289,8 @@ You can also specify the kind with ` + "`-k KIND`" + `.
|
||||
If archetypes are provided in your theme or site, they will be used.
|
||||
|
||||
Ensure you run this within the root directory of your site.`
|
||||
|
||||
cmd.RunE = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -341,7 +348,7 @@ description = ""
|
||||
homepage = "http://example.com/"
|
||||
tags = []
|
||||
features = []
|
||||
min_version = "0.114.1"
|
||||
min_version = "0.115.0"
|
||||
|
||||
[author]
|
||||
name = ""
|
||||
|
||||
@@ -66,6 +66,10 @@ func Append(to any, from ...any) (any, error) {
|
||||
|
||||
if len(from) == 1 {
|
||||
fromv := reflect.ValueOf(from[0])
|
||||
if !fromv.IsValid() {
|
||||
// from[0] is nil
|
||||
return appendToInterfaceSliceFromValues(tov, fromv)
|
||||
}
|
||||
fromt := fromv.Type()
|
||||
if fromt.Kind() == reflect.Slice {
|
||||
fromt = fromt.Elem()
|
||||
@@ -94,7 +98,7 @@ func Append(to any, from ...any) (any, error) {
|
||||
|
||||
for _, f := range from {
|
||||
fv := reflect.ValueOf(f)
|
||||
if !fv.Type().AssignableTo(tot) {
|
||||
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
|
||||
// Fall back to a []interface{} slice.
|
||||
tov, _ := indirect(reflect.ValueOf(to))
|
||||
return appendToInterfaceSlice(tov, from...)
|
||||
@@ -109,6 +113,10 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
|
||||
var tos []any
|
||||
|
||||
for _, slice := range []reflect.Value{slice1, slice2} {
|
||||
if !slice.IsValid() {
|
||||
tos = append(tos, nil)
|
||||
continue
|
||||
}
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
tos = append(tos, slice.Index(i).Interface())
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ func TestAppend(t *testing.T) {
|
||||
[]any{"c"},
|
||||
false,
|
||||
},
|
||||
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
|
||||
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
|
||||
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
|
||||
} {
|
||||
|
||||
result, err := Append(test.start, test.addend...)
|
||||
|
||||
@@ -59,11 +59,34 @@ func GetGID() uint64 {
|
||||
return n
|
||||
}
|
||||
|
||||
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
|
||||
func IsFeatureNotAvailableError(err error) bool {
|
||||
return errors.Is(err, &FeatureNotAvailableError{})
|
||||
}
|
||||
|
||||
// ErrFeatureNotAvailable denotes that a feature is unavailable.
|
||||
//
|
||||
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
|
||||
// and this error is used to signal those situations.
|
||||
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
|
||||
var ErrFeatureNotAvailable = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
|
||||
|
||||
// FeatureNotAvailableError is an error type used to signal that a feature is not available.
|
||||
type FeatureNotAvailableError struct {
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Error() string {
|
||||
return e.Cause.Error()
|
||||
}
|
||||
|
||||
func (e *FeatureNotAvailableError) Is(target error) bool {
|
||||
_, ok := target.(*FeatureNotAvailableError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Must panics if err != nil.
|
||||
func Must(err error) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package herrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -34,3 +35,12 @@ func TestIsNotExist(t *testing.T) {
|
||||
// os.IsNotExist returns false for wrapped errors.
|
||||
c.Assert(IsNotExist(fmt.Errorf("foo: %w", afero.ErrFileNotFound)), qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestIsFeatureNotAvailableError(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
|
||||
c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/clocks"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/locales"
|
||||
@@ -75,7 +75,7 @@ var (
|
||||
"December",
|
||||
}
|
||||
|
||||
Clock = clock.System()
|
||||
Clock = clocks.System()
|
||||
)
|
||||
|
||||
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 114,
|
||||
PatchLevel: 1,
|
||||
Suffix: "",
|
||||
Minor: 116,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ type Config struct {
|
||||
Minify minifiers.MinifyConfig `mapstructure:"-"`
|
||||
|
||||
// Permalink configuration.
|
||||
Permalinks map[string]string `mapstructure:"-"`
|
||||
Permalinks map[string]map[string]string `mapstructure:"-"`
|
||||
|
||||
// Taxonomy configuration.
|
||||
Taxonomies map[string]string `mapstructure:"-"`
|
||||
@@ -274,6 +274,14 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
}
|
||||
disabledLangs[lang] = true
|
||||
}
|
||||
for lang, language := range c.Languages {
|
||||
if language.Disabled {
|
||||
disabledLangs[lang] = true
|
||||
if lang == c.DefaultContentLanguage {
|
||||
return fmt.Errorf("cannot disable default content language %q", lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignoredErrors := make(map[string]bool)
|
||||
for _, err := range c.IgnoreErrors {
|
||||
@@ -484,12 +492,6 @@ type RootConfig struct {
|
||||
// Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings.
|
||||
EnableMissingTranslationPlaceholders bool
|
||||
|
||||
// Enable to print warnings for missing translation strings.
|
||||
LogI18nWarnings bool
|
||||
|
||||
// ENable to print warnings for multiple files published to the same destination.
|
||||
LogPathWarnings bool
|
||||
|
||||
// Enable to panic on warning log entries. This may make it easier to detect the source.
|
||||
PanicOnWarning bool
|
||||
|
||||
@@ -525,6 +527,12 @@ type RootConfig struct {
|
||||
// Whether to track and print unused templates during the build.
|
||||
PrintUnusedTemplates bool
|
||||
|
||||
// Enable to print warnings for missing translation strings.
|
||||
PrintI18nWarnings bool
|
||||
|
||||
// ENable to print warnings for multiple files published to the same destination.
|
||||
PrintPathWarnings bool
|
||||
|
||||
// URL to be used as a placeholder when a page reference cannot be found in ref or relref. Is used as-is.
|
||||
RefLinksNotFoundURL string
|
||||
|
||||
@@ -545,7 +553,7 @@ type RootConfig struct {
|
||||
// See Modules for more a more flexible way to load themes.
|
||||
Theme []string
|
||||
|
||||
// Timeout for generating page contents, specified as a duration or in milliseconds.
|
||||
// Timeout for generating page contents, specified as a duration or in seconds.
|
||||
Timeout string
|
||||
|
||||
// The time zone (or location), e.g. Europe/Oslo, used to parse front matter dates without such information and in the time function.
|
||||
|
||||
@@ -138,8 +138,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"mediaTypes": {
|
||||
key: "mediaTypes",
|
||||
"mediatypes": {
|
||||
key: "mediatypes",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key))
|
||||
@@ -150,7 +150,7 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
key: "outputs",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
|
||||
m := p.p.GetStringMap("outputs")
|
||||
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
|
||||
p.c.Outputs = make(map[string][]string)
|
||||
for k, v := range m {
|
||||
s := types.ToStringSlicePreserveString(v)
|
||||
@@ -168,8 +168,8 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return nil
|
||||
},
|
||||
},
|
||||
"outputFormats": {
|
||||
key: "outputFormats",
|
||||
"outputformats": {
|
||||
key: "outputformats",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
var err error
|
||||
p.c.OutputFormats, err = output.DecodeConfig(p.c.MediaTypes.Config, p.p.Get(d.key))
|
||||
@@ -206,8 +206,9 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
"permalinks": {
|
||||
key: "permalinks",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Permalinks = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
|
||||
return nil
|
||||
var err error
|
||||
p.c.Permalinks, err = page.DecodePermalinksConfig(p.p.GetStringMap(d.key))
|
||||
return err
|
||||
},
|
||||
},
|
||||
"sitemap": {
|
||||
@@ -369,3 +370,19 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for k, v := range allDecoderSetups {
|
||||
// Verify that k and v.key is all lower case.
|
||||
if k != strings.ToLower(k) {
|
||||
panic(fmt.Sprintf("key %q is not lower case", k))
|
||||
}
|
||||
if v.key != strings.ToLower(v.key) {
|
||||
panic(fmt.Sprintf("key %q is not lower case", v.key))
|
||||
}
|
||||
|
||||
if k != v.key {
|
||||
panic(fmt.Sprintf("key %q is not the same as the map key %q", k, v.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,16 @@ func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
|
||||
return c.m.LanguagesDefaultFirst
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LanguagePrefix() string {
|
||||
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.Language().Lang {
|
||||
return c.Language().Lang
|
||||
}
|
||||
if !c.IsMultiLingual() || c.DefaultContentLanguage() == c.Language().Lang {
|
||||
return ""
|
||||
}
|
||||
return c.Language().Lang
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) BaseURL() urls.BaseURL {
|
||||
return c.config.C.BaseURL
|
||||
}
|
||||
@@ -199,8 +209,8 @@ func (c ConfigLanguage) EnableMissingTranslationPlaceholders() bool {
|
||||
return c.config.EnableMissingTranslationPlaceholders
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) LogI18nWarnings() bool {
|
||||
return c.config.LogI18nWarnings
|
||||
func (c ConfigLanguage) PrintI18nWarnings() bool {
|
||||
return c.config.PrintI18nWarnings
|
||||
}
|
||||
|
||||
func (c ConfigLanguage) CreateTitle(s string) string {
|
||||
|
||||
@@ -69,3 +69,21 @@ Title: {{ .Title }}
|
||||
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
|
||||
|
||||
}
|
||||
|
||||
func TestConfigAliases(t *testing.T) {
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
logI18nWarnings = true
|
||||
logPathWarnings = true
|
||||
`
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
|
||||
).Build()
|
||||
|
||||
conf := b.H.Configs.Base
|
||||
|
||||
b.Assert(conf.PrintI18nWarnings, qt.Equals, true)
|
||||
b.Assert(conf.PrintPathWarnings, qt.Equals, true)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,11 @@ type configLoader struct {
|
||||
|
||||
// Handle some legacy values.
|
||||
func (l configLoader) applyConfigAliases() error {
|
||||
aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
|
||||
aliases := []types.KeyValueStr{
|
||||
{Key: "indexes", Value: "taxonomies"},
|
||||
{Key: "logI18nWarnings", Value: "printI18nWarnings"},
|
||||
{Key: "logPathWarnings", Value: "printPathWarnings"},
|
||||
}
|
||||
|
||||
for _, alias := range aliases {
|
||||
if l.cfg.IsSet(alias.Key) {
|
||||
@@ -289,11 +293,19 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
|
||||
} else {
|
||||
l.cfg.Set(env.Key, val)
|
||||
}
|
||||
} else if nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
// The container does not exist yet.
|
||||
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
|
||||
if nestedKey != "" {
|
||||
owner[nestedKey] = env.Value
|
||||
} else {
|
||||
var val any = env.Value
|
||||
if _, ok := allDecoderSetups[env.Key]; ok {
|
||||
// A map.
|
||||
val, err = metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{})
|
||||
}
|
||||
if err == nil {
|
||||
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-4
@@ -82,7 +82,7 @@ type LoadConfigResult struct {
|
||||
|
||||
var defaultBuild = BuildConfig{
|
||||
UseResourceCacheWhen: "fallback",
|
||||
WriteStats: false,
|
||||
BuildStats: BuildStats{},
|
||||
|
||||
CacheBusters: []CacheBuster{
|
||||
{
|
||||
@@ -111,7 +111,8 @@ type BuildConfig struct {
|
||||
|
||||
// When enabled, will collect and write a hugo_stats.json with some build
|
||||
// related aggregated data (e.g. CSS class names).
|
||||
WriteStats bool
|
||||
// Note that this was a bool <= v0.115.0.
|
||||
BuildStats BuildStats
|
||||
|
||||
// Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js
|
||||
// file.
|
||||
@@ -121,6 +122,21 @@ type BuildConfig struct {
|
||||
CacheBusters []CacheBuster
|
||||
}
|
||||
|
||||
// BuildStats configures if and what to write to the hugo_stats.json file.
|
||||
type BuildStats struct {
|
||||
Enable bool
|
||||
DisableTags bool
|
||||
DisableClasses bool
|
||||
DisableIDs bool
|
||||
}
|
||||
|
||||
func (w BuildStats) Enabled() bool {
|
||||
if !w.Enable {
|
||||
return false
|
||||
}
|
||||
return !w.DisableTags || !w.DisableClasses || !w.DisableIDs
|
||||
}
|
||||
|
||||
func (b BuildConfig) clone() BuildConfig {
|
||||
b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
|
||||
return b
|
||||
@@ -132,7 +148,7 @@ func (b BuildConfig) UseResourceCache(err error) bool {
|
||||
}
|
||||
|
||||
if b.UseResourceCacheWhen == "fallback" {
|
||||
return err == herrors.ErrFeatureNotAvailable
|
||||
return herrors.IsFeatureNotAvailableError(err)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -171,14 +187,22 @@ func (b *BuildConfig) CompileConfig(logger loggers.Logger) error {
|
||||
|
||||
func DecodeBuildConfig(cfg Provider) BuildConfig {
|
||||
m := cfg.GetStringMap("build")
|
||||
|
||||
b := defaultBuild.clone()
|
||||
if m == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
// writeStats was a bool <= v0.115.0.
|
||||
if writeStats, ok := m["writestats"]; ok {
|
||||
if bb, ok := writeStats.(bool); ok {
|
||||
m["buildstats"] = BuildStats{Enable: bb}
|
||||
}
|
||||
}
|
||||
|
||||
err := mapstructure.WeakDecode(m, &b)
|
||||
if err != nil {
|
||||
return defaultBuild
|
||||
return b
|
||||
}
|
||||
|
||||
b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen)
|
||||
|
||||
@@ -27,6 +27,7 @@ type AllProvider interface {
|
||||
Language() *langs.Language
|
||||
Languages() langs.Languages
|
||||
LanguagesDefaultFirst() langs.Languages
|
||||
LanguagePrefix() string
|
||||
BaseURL() urls.BaseURL
|
||||
BaseURLLiveReload() urls.BaseURL
|
||||
Environment() string
|
||||
@@ -57,7 +58,7 @@ type AllProvider interface {
|
||||
EnableMissingTranslationPlaceholders() bool
|
||||
TemplateMetrics() bool
|
||||
TemplateMetricsHints() bool
|
||||
LogI18nWarnings() bool
|
||||
PrintI18nWarnings() bool
|
||||
CreateTitle(s string) string
|
||||
IgnoreFile(s string) bool
|
||||
NewContentEditor() string
|
||||
|
||||
@@ -34,7 +34,7 @@ const securityConfigKey = "security"
|
||||
// DefaultConfig holds the default security policy.
|
||||
var DefaultConfig = Config{
|
||||
Exec: Exec{
|
||||
Allow: NewWhitelist(
|
||||
Allow: MustNewWhitelist(
|
||||
"^(dart-)?sass(-embedded)?$", // sass, dart-sass, dart-sass-embedded.
|
||||
"^go$", // for Go Modules
|
||||
"^npx$", // used by all Node tools (Babel, PostCSS).
|
||||
@@ -42,14 +42,14 @@ var DefaultConfig = Config{
|
||||
),
|
||||
// These have been tested to work with Hugo's external programs
|
||||
// on Windows, Linux and MacOS.
|
||||
OsEnv: NewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+)$`),
|
||||
OsEnv: MustNewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$`),
|
||||
},
|
||||
Funcs: Funcs{
|
||||
Getenv: NewWhitelist("^HUGO_", "^CI$"),
|
||||
Getenv: MustNewWhitelist("^HUGO_", "^CI$"),
|
||||
},
|
||||
HTTP: HTTP{
|
||||
URLs: NewWhitelist(".*"),
|
||||
Methods: NewWhitelist("(?i)GET|POST"),
|
||||
URLs: MustNewWhitelist(".*"),
|
||||
Methods: MustNewWhitelist("(?i)GET|POST"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
|
||||
|
||||
wl := types.ToStringSlicePreserveString(data)
|
||||
|
||||
return NewWhitelist(wl...), nil
|
||||
return NewWhitelist(wl...)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestToTOML(t *testing.T) {
|
||||
got := DefaultConfig.ToTOML()
|
||||
|
||||
c.Assert(got, qt.Equals,
|
||||
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
|
||||
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^npx$', '^postcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.goTemplates]\n AllowActionJSTmpl = false\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,9 +154,6 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
c.Assert(pc.Exec.Allow.Accept("a"), qt.IsFalse)
|
||||
c.Assert(pc.Exec.Allow.Accept("npx"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.Allow.Accept("Npx"), qt.IsFalse)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
|
||||
|
||||
c.Assert(pc.HTTP.URLs.Accept("https://example.org"), qt.IsTrue)
|
||||
c.Assert(pc.HTTP.Methods.Accept("POST"), qt.IsTrue)
|
||||
@@ -167,6 +164,10 @@ func TestDecodeConfigDefault(t *testing.T) {
|
||||
|
||||
c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("GOROOT"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("HOME"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("SSH_AUTH_SOCK"), qt.IsTrue)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("a"), qt.IsFalse)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse)
|
||||
c.Assert(pc.Exec.OsEnv.Accept("MYSECRET"), qt.IsFalse)
|
||||
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@ func (w Whitelist) MarshalJSON() ([]byte, error) {
|
||||
// NewWhitelist creates a new Whitelist from zero or more patterns.
|
||||
// An empty patterns list or a pattern with the value 'none' will create
|
||||
// a whitelist that will Accept none.
|
||||
func NewWhitelist(patterns ...string) Whitelist {
|
||||
func NewWhitelist(patterns ...string) (Whitelist, error) {
|
||||
if len(patterns) == 0 {
|
||||
return Whitelist{acceptNone: true}
|
||||
return Whitelist{acceptNone: true}, nil
|
||||
}
|
||||
|
||||
var acceptSome bool
|
||||
@@ -68,7 +68,7 @@ func NewWhitelist(patterns ...string) Whitelist {
|
||||
if !acceptSome {
|
||||
return Whitelist{
|
||||
acceptNone: true,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
var patternsr []*regexp.Regexp
|
||||
@@ -78,10 +78,23 @@ func NewWhitelist(patterns ...string) Whitelist {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
patternsr = append(patternsr, regexp.MustCompile(p))
|
||||
re, err := regexp.Compile(p)
|
||||
if err != nil {
|
||||
return Whitelist{}, fmt.Errorf("failed to compile whitelist pattern %q: %w", p, err)
|
||||
}
|
||||
patternsr = append(patternsr, re)
|
||||
}
|
||||
|
||||
return Whitelist{patterns: patternsr, patternsStrings: patternsStrings}
|
||||
return Whitelist{patterns: patternsr, patternsStrings: patternsStrings}, nil
|
||||
}
|
||||
|
||||
// MustNewWhitelist creates a new Whitelist from zero or more patterns and panics on error.
|
||||
func MustNewWhitelist(patterns ...string) Whitelist {
|
||||
w, err := NewWhitelist(patterns...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Accept reports whether name is whitelisted.
|
||||
|
||||
@@ -24,21 +24,21 @@ func TestWhitelist(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
c.Run("none", func(c *qt.C) {
|
||||
c.Assert(NewWhitelist("none", "foo").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(NewWhitelist().Accept("foo"), qt.IsFalse)
|
||||
c.Assert(NewWhitelist("").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(NewWhitelist(" ", " ").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(MustNewWhitelist("none", "foo").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(MustNewWhitelist().Accept("foo"), qt.IsFalse)
|
||||
c.Assert(MustNewWhitelist("").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(MustNewWhitelist(" ", " ").Accept("foo"), qt.IsFalse)
|
||||
c.Assert(Whitelist{}.Accept("foo"), qt.IsFalse)
|
||||
})
|
||||
|
||||
c.Run("One", func(c *qt.C) {
|
||||
w := NewWhitelist("^foo.*")
|
||||
w := MustNewWhitelist("^foo.*")
|
||||
c.Assert(w.Accept("foo"), qt.IsTrue)
|
||||
c.Assert(w.Accept("mfoo"), qt.IsFalse)
|
||||
})
|
||||
|
||||
c.Run("Multiple", func(c *qt.C) {
|
||||
w := NewWhitelist("^foo.*", "^bar.*")
|
||||
w := MustNewWhitelist("^foo.*", "^bar.*")
|
||||
c.Assert(w.Accept("foo"), qt.IsTrue)
|
||||
c.Assert(w.Accept("bar"), qt.IsTrue)
|
||||
c.Assert(w.Accept("mbar"), qt.IsFalse)
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build !nodeploy
|
||||
// +build !nodeploy
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
|
||||
@@ -89,67 +89,149 @@ If you set both `slug` and `url` in front matter, the `url` value takes preceden
|
||||
|
||||
### Permalinks
|
||||
|
||||
In your site configuration, set a URL pattern for regular pages within a top-level section. This is recursive, affecting descendant regular pages.
|
||||
In your site configuration, define a URL pattern for each top-level section. Each URL pattern can target a given language and/or [page kind].
|
||||
|
||||
{{% note %}}
|
||||
The `permalinks` defined in your site configuration do not apply to section pages. To adjust the URL for section pages, set `url` in front matter.
|
||||
{{% /note %}}
|
||||
Front matter `url` values override the URL patterns defined in the `permalinks` section of your site configuration.
|
||||
|
||||
#### Examples {#permalinks-examples}
|
||||
[page kind]: https://gohugo.io/templates/section-templates/#page-kinds
|
||||
|
||||
#### Monolingual examples {#permalinks-monolingual-examples}
|
||||
|
||||
With this content structure:
|
||||
|
||||
```text
|
||||
content/
|
||||
├── posts/
|
||||
│ ├── _index.md
|
||||
│ ├── post-1.md
|
||||
│ └── post-2.md
|
||||
│ ├── bash-in-slow-motion.md
|
||||
│ └── tls-in-a-nutshell.md
|
||||
├── tutorials/
|
||||
│ ├── git-for-beginners.md
|
||||
│ └── javascript-bundling-with-hugo.md
|
||||
└── _index.md
|
||||
```
|
||||
|
||||
Create a date-based hierarchy, recursively, for regular pages within the `posts` section:
|
||||
Render tutorials under "training", and render the posts under "articles" with a date-base hierarchy:
|
||||
|
||||
{{< code-toggle file="hugo" copy=false >}}
|
||||
[permalinks]
|
||||
posts = '/posts/:year/:month/:title/'
|
||||
[permalinks.page]
|
||||
posts = '/articles/:year/:month/:slug/'
|
||||
tutorials = '/training/:slug/'
|
||||
[permalinks.section]
|
||||
posts = '/articles/'
|
||||
tutorials = '/training/'
|
||||
{{< /code-toggle >}}
|
||||
|
||||
The structure of the published site will be:
|
||||
|
||||
```text
|
||||
public/
|
||||
├── posts/
|
||||
│ ├── 2023/
|
||||
│ │ └── 03/
|
||||
│ │ ├── post-1/
|
||||
│ │ │ └── index.html
|
||||
│ │ └── post-2/
|
||||
│ │ └── index.html
|
||||
│ └── index.html
|
||||
├── favicon.ico
|
||||
├── articles/
|
||||
│ ├── 2023/
|
||||
│ │ ├── 04/
|
||||
│ │ │ └── bash-in-slow-motion/
|
||||
│ │ │ └── index.html
|
||||
│ │ └── 06/
|
||||
│ │ └── tls-in-a-nutshell/
|
||||
│ │ └── index.html
|
||||
│ └── index.html
|
||||
├── training/
|
||||
│ ├── git-for-beginners/
|
||||
│ │ └── index.html
|
||||
│ ├── javascript-bundling-with-hugo/
|
||||
│ │ └── index.html
|
||||
│ └── index.html
|
||||
└── index.html
|
||||
```
|
||||
|
||||
To create a date-based hierarchy for regular pages in the content root:
|
||||
|
||||
{{< code-toggle file="hugo" copy=false >}}
|
||||
[permalinks]
|
||||
'/' = '/:year/:month/:title/'
|
||||
[permalinks.page]
|
||||
"/" = "/:year/:month/:slug/"
|
||||
{{< /code-toggle >}}
|
||||
|
||||
{{% note %}}
|
||||
A URL pattern defined for the content root is not recursive.
|
||||
{{% /note %}}
|
||||
|
||||
Use the same approach with taxonomies. For example, to omit the taxonomy segment of the URL:
|
||||
Use the same approach with taxonomy terms. For example, to omit the taxonomy segment of the URL:
|
||||
|
||||
{{< code-toggle file="hugo" copy=false >}}
|
||||
[permalinks]
|
||||
'tags' = '/:title/'
|
||||
[permalinks.term]
|
||||
'tags' = '/:slug/'
|
||||
{{< /code-toggle >}}
|
||||
|
||||
Front matter `url` values take precedence over URL patterns defined in `permalinks`.
|
||||
#### Multilingual example {#permalinks-multilingual-example}
|
||||
|
||||
Use the `permalinks` configuration as a component of your localization strategy.
|
||||
|
||||
With this content structure:
|
||||
|
||||
```text
|
||||
content/
|
||||
├── de/
|
||||
│ ├── books/
|
||||
│ │ ├── les-miserables.md
|
||||
│ │ └── the-hunchback-of-notre-dame.md
|
||||
│ └── _index.md
|
||||
└── en/
|
||||
├── books/
|
||||
│ ├── les-miserables.md
|
||||
│ └── the-hunchback-of-notre-dame.md
|
||||
└── _index.md
|
||||
```
|
||||
|
||||
And this site configuration:
|
||||
|
||||
{{< code-toggle file="hugo" copy=false >}}
|
||||
defaultContentLanguage = 'en'
|
||||
defaultContentLanguageInSubdir = true
|
||||
|
||||
[languages.en]
|
||||
contentDir = 'content/en'
|
||||
languageCode = 'en-US'
|
||||
languageDirection = 'ltr'
|
||||
languageName = 'English'
|
||||
weight = 1
|
||||
|
||||
[languages.en.permalinks.page]
|
||||
books = "/books/:slug/"
|
||||
|
||||
[languages.en.permalinks.section]
|
||||
books = "/books/"
|
||||
|
||||
[languages.es]
|
||||
contentDir = 'content/de'
|
||||
languageCode = 'es-ES'
|
||||
languageDirection = 'ltr'
|
||||
languageName = 'Español'
|
||||
weight = 2
|
||||
|
||||
[languages.es.permalinks.page]
|
||||
books = "/libros/:slug/"
|
||||
|
||||
[languages.es.permalinks.section]
|
||||
books = "/libros/"
|
||||
{{< /code-toggle >}}
|
||||
|
||||
The structure of the published site will be:
|
||||
|
||||
```text
|
||||
public/
|
||||
├── en/
|
||||
│ ├── books/
|
||||
│ │ ├── les-miserables/
|
||||
│ │ │ └── index.html
|
||||
│ │ ├── the-hunchback-of-notre-dame/
|
||||
│ │ │ └── index.html
|
||||
│ │ └── index.html
|
||||
│ └── index.html
|
||||
├── es/
|
||||
│ ├── libros/
|
||||
│ │ ├── les-miserables/
|
||||
│ │ │ └── index.html
|
||||
│ │ ├── the-hunchback-of-notre-dame/
|
||||
│ │ │ └── index.html
|
||||
│ │ └── index.html
|
||||
│ └── index.html
|
||||
└── index.html
|
||||
````
|
||||
|
||||
#### Tokens
|
||||
|
||||
|
||||
@@ -623,7 +623,13 @@ status = 404
|
||||
|
||||
Set `titleCaseStyle` to specify the title style used by the [title](/functions/title/) template function and the automatic section titles in Hugo.
|
||||
|
||||
By default, Hugo adheres to the capitalization rules in the [Associated Press (AP) Stylebook]. Set `titleCaseStyle` to `chicago` if you would prefer to follow the [Chicago Manual of Style], or set if to `go` to use Go's convention of capitalizing every word.
|
||||
Can be one of:
|
||||
|
||||
* `ap` (default), the capitalization rules in the [Associated Press (AP) Stylebook]
|
||||
* `chicago`, the [Chicago Manual of Style]
|
||||
* `go`, Go's convention of capitalizing every word.
|
||||
* `firstupper`, capitalize the first letter of the first word.
|
||||
* `none`, no capitalization.
|
||||
|
||||
[Associated Press (AP) Stylebook]: https://www.apstylebook.com/
|
||||
[Chicago Manual of Style]: https://www.chicagomanualofstyle.org/home.html
|
||||
|
||||
+45
-2
@@ -73,6 +73,12 @@
|
||||
"applescript"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ArangoDB AQL",
|
||||
"Aliases": [
|
||||
"aql"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Arduino",
|
||||
"Aliases": [
|
||||
@@ -85,6 +91,19 @@
|
||||
"armasm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "AutoHotkey",
|
||||
"Aliases": [
|
||||
"autohotkey",
|
||||
"ahk"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "AutoIt",
|
||||
"Aliases": [
|
||||
"autoit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Awk",
|
||||
"Aliases": [
|
||||
@@ -666,6 +685,12 @@
|
||||
"io"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "ISCdhcpd",
|
||||
"Aliases": [
|
||||
"iscdhcpd"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "J",
|
||||
"Aliases": [
|
||||
@@ -1230,6 +1255,12 @@
|
||||
"sieve"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Smali",
|
||||
"Aliases": [
|
||||
"smali"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Smalltalk",
|
||||
"Aliases": [
|
||||
@@ -1324,6 +1355,13 @@
|
||||
"tablegen"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Tal",
|
||||
"Aliases": [
|
||||
"tal",
|
||||
"uxntal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "TASM",
|
||||
"Aliases": [
|
||||
@@ -1505,6 +1543,12 @@
|
||||
"Name": "WDTE",
|
||||
"Aliases": null
|
||||
},
|
||||
{
|
||||
"Name": "WebGPU Shading Language",
|
||||
"Aliases": [
|
||||
"wgsl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "Whiley",
|
||||
"Aliases": [
|
||||
@@ -1723,7 +1767,6 @@
|
||||
"js": {
|
||||
"precision": 0,
|
||||
"keepVarNames": false,
|
||||
"noNullishOperator": false,
|
||||
"version": 2022
|
||||
},
|
||||
"json": {
|
||||
@@ -1749,7 +1792,7 @@
|
||||
"^postcss$"
|
||||
],
|
||||
"osEnv": [
|
||||
"(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$"
|
||||
"(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$"
|
||||
]
|
||||
},
|
||||
"funcs": {
|
||||
|
||||
@@ -3,10 +3,10 @@ module github.com/gohugoio/hugo
|
||||
require (
|
||||
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69
|
||||
github.com/PuerkitoBio/purell v1.1.1
|
||||
github.com/alecthomas/chroma/v2 v2.7.0
|
||||
github.com/alecthomas/chroma/v2 v2.8.0
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/aws/aws-sdk-go v1.43.5
|
||||
github.com/bep/clock v0.3.0
|
||||
github.com/bep/clocks v0.5.0
|
||||
github.com/bep/debounce v1.2.0
|
||||
github.com/bep/gitmap v1.1.2
|
||||
github.com/bep/goat v0.5.0
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/disintegration/gift v1.2.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/evanw/esbuild v0.18.5
|
||||
github.com/evanw/esbuild v0.18.11
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/frankban/quicktest v1.14.5
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/mitchellh/hashstructure v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/muesli/smartcrop v0.3.0
|
||||
github.com/niklasfasching/go-org v1.6.6
|
||||
github.com/niklasfasching/go-org v1.7.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/pelletier/go-toml/v2 v2.0.8
|
||||
github.com/rogpeppe/go-internal v1.10.1-0.20230508101108-a4f6fabd84c5
|
||||
@@ -104,7 +104,6 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/bep/clocks v0.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
|
||||
@@ -126,8 +126,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI=
|
||||
github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264=
|
||||
github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
|
||||
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
@@ -174,8 +174,6 @@ github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAm
|
||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bep/clock v0.3.0 h1:vfOA6+wVb6pPQEiXow9f/too92vNTLe9MuwO13PfI0M=
|
||||
github.com/bep/clock v0.3.0/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlwk=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
|
||||
@@ -260,8 +258,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanw/esbuild v0.18.5 h1:B474TK1dD7ZQJOEE7BKqCmTerOYEIzxVH35UY4MOM0s=
|
||||
github.com/evanw/esbuild v0.18.5/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
|
||||
github.com/evanw/esbuild v0.18.11 h1:Mb0qb9KyJQraob0Y7LA0DOdD8ijJs2TxhqgqrDeGq4w=
|
||||
github.com/evanw/esbuild v0.18.11/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
@@ -504,8 +502,8 @@ github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78Rwc
|
||||
github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niklasfasching/go-org v1.6.6 h1:U6+mJ80p3weR4oP+Z+Pb2EVkSbt1MUwweBbUcF1hVqQ=
|
||||
github.com/niklasfasching/go-org v1.6.6/go.mod h1:o3pMQpO9n6RNBXz2Oc2DiRkaVwjns0JElyKiG7yXwA4=
|
||||
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
|
||||
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
|
||||
@@ -205,6 +205,8 @@ func ReaderContains(r io.Reader, subslice []byte) bool {
|
||||
// - "Go" (strings.Title)
|
||||
// - "AP" (see https://www.apstylebook.com/)
|
||||
// - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
|
||||
// - "FirstUpper" (only the first character is upper case)
|
||||
// - "None" (no transformation)
|
||||
//
|
||||
// If an unknown or empty style is provided, AP style is what you get.
|
||||
func GetTitleFunc(style string) func(s string) string {
|
||||
@@ -214,6 +216,10 @@ func GetTitleFunc(style string) func(s string) string {
|
||||
case "chicago":
|
||||
tc := transform.NewTitleConverter(transform.ChicagoStyle)
|
||||
return tc.Title
|
||||
case "none":
|
||||
return func(s string) string { return s }
|
||||
case "firstupper":
|
||||
return FirstUpper
|
||||
default:
|
||||
tc := transform.NewTitleConverter(transform.APStyle)
|
||||
return tc.Title
|
||||
|
||||
@@ -203,7 +203,7 @@ func TestReaderContains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTitleFunc(t *testing.T) {
|
||||
title := "somewhere over the rainbow"
|
||||
title := "somewhere over the Rainbow"
|
||||
c := qt.New(t)
|
||||
|
||||
c.Assert(helpers.GetTitleFunc("go")(title), qt.Equals, "Somewhere Over The Rainbow")
|
||||
@@ -213,6 +213,8 @@ func TestGetTitleFunc(t *testing.T) {
|
||||
c.Assert(helpers.GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
|
||||
c.Assert(helpers.GetTitleFunc("")(title), qt.Equals, "Somewhere Over the Rainbow")
|
||||
c.Assert(helpers.GetTitleFunc("unknown")(title), qt.Equals, "Somewhere Over the Rainbow")
|
||||
c.Assert(helpers.GetTitleFunc("none")(title), qt.Equals, title)
|
||||
c.Assert(helpers.GetTitleFunc("firstupper")(title), qt.Equals, "Somewhere over the Rainbow")
|
||||
}
|
||||
|
||||
func BenchmarkReaderContains(b *testing.B) {
|
||||
|
||||
+39
-18
@@ -109,8 +109,16 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
|
||||
"weight": 10,
|
||||
},
|
||||
})
|
||||
v.Set("defaultContentLanguage", "en")
|
||||
} else {
|
||||
v.Set("defaultContentLanguage", lang)
|
||||
v.Set("languages", map[string]any{
|
||||
lang: map[string]interface{}{
|
||||
"weight": 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
v.Set("defaultContentLanguage", "en")
|
||||
|
||||
v.Set("defaultContentLanguageInSubdir", defaultInSubDir)
|
||||
v.Set("baseURL", test.baseURL)
|
||||
|
||||
@@ -118,16 +126,21 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
|
||||
if multilingual {
|
||||
configLang = lang
|
||||
}
|
||||
defaultContentLanguage := lang
|
||||
if multilingual {
|
||||
defaultContentLanguage = "en"
|
||||
}
|
||||
|
||||
p := newTestPathSpecFromCfgAndLang(v, configLang)
|
||||
|
||||
output := p.AbsURL(test.input, addLanguage)
|
||||
expected := test.expected
|
||||
if multilingual && addLanguage {
|
||||
if !defaultInSubDir && lang == "en" {
|
||||
expected = strings.Replace(expected, "MULTI", "", 1)
|
||||
} else {
|
||||
expected = strings.Replace(expected, "MULTI", lang+"/", 1)
|
||||
}
|
||||
if addLanguage {
|
||||
addLanguage = defaultInSubDir && lang == defaultContentLanguage
|
||||
addLanguage = addLanguage || (lang != defaultContentLanguage && multilingual)
|
||||
}
|
||||
if addLanguage {
|
||||
expected = strings.Replace(expected, "MULTI", lang+"/", 1)
|
||||
} else {
|
||||
expected = strings.Replace(expected, "MULTI", "", 1)
|
||||
}
|
||||
@@ -162,8 +175,16 @@ func doTestRelURL(t testing.TB, defaultInSubDir, addLanguage, multilingual bool,
|
||||
"weight": 10,
|
||||
},
|
||||
})
|
||||
v.Set("defaultContentLanguage", "en")
|
||||
} else {
|
||||
v.Set("defaultContentLanguage", lang)
|
||||
v.Set("languages", map[string]any{
|
||||
lang: map[string]interface{}{
|
||||
"weight": 10,
|
||||
},
|
||||
})
|
||||
}
|
||||
v.Set("defaultContentLanguage", "en")
|
||||
|
||||
v.Set("defaultContentLanguageInSubdir", defaultInSubDir)
|
||||
|
||||
tests := []struct {
|
||||
@@ -209,25 +230,25 @@ func doTestRelURL(t testing.TB, defaultInSubDir, addLanguage, multilingual bool,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
c.Run(fmt.Sprintf("%v/%t%t%t/%s", test, defaultInSubDir, addLanguage, multilingual, lang), func(c *qt.C) {
|
||||
c.Run(fmt.Sprintf("%v/defaultInSubDir=%t;addLanguage=%t;multilingual=%t/%s", test, defaultInSubDir, addLanguage, multilingual, lang), func(c *qt.C) {
|
||||
|
||||
v.Set("baseURL", test.baseURL)
|
||||
v.Set("canonifyURLs", test.canonify)
|
||||
var configLang string
|
||||
defaultContentLanguage := lang
|
||||
if multilingual {
|
||||
configLang = lang
|
||||
defaultContentLanguage = "en"
|
||||
}
|
||||
p := newTestPathSpecFromCfgAndLang(v, configLang)
|
||||
p := newTestPathSpecFromCfgAndLang(v, lang)
|
||||
|
||||
output := p.RelURL(test.input, addLanguage)
|
||||
|
||||
expected := test.expected
|
||||
if multilingual && addLanguage {
|
||||
if !defaultInSubDir && lang == "en" {
|
||||
expected = strings.Replace(expected, "MULTI", "", 1)
|
||||
} else {
|
||||
expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
|
||||
}
|
||||
if addLanguage {
|
||||
addLanguage = defaultInSubDir && lang == defaultContentLanguage
|
||||
addLanguage = addLanguage || (lang != defaultContentLanguage && multilingual)
|
||||
}
|
||||
if addLanguage {
|
||||
expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
|
||||
} else {
|
||||
expected = strings.Replace(expected, "MULTI", "", 1)
|
||||
}
|
||||
|
||||
@@ -1144,6 +1144,48 @@ LanguageCode: {{ .Site.LanguageCode }}|{{ site.Language.LanguageCode }}|
|
||||
|
||||
}
|
||||
|
||||
// See #11159
|
||||
func TestConfigOutputFormatsPerLanguage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[languages]
|
||||
[languages.en]
|
||||
title = "English Title"
|
||||
[languages.sv]
|
||||
title = "Swedish Title"
|
||||
[languages.sv.outputFormats.html]
|
||||
path = "foo"
|
||||
[languages.sv.mediatypes."text/html"]
|
||||
suffixes = ["bar"]
|
||||
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
|
||||
|
||||
`
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "Home.")
|
||||
|
||||
enConfig := b.H.Sites[0].conf
|
||||
m, _ := enConfig.MediaTypes.Config.GetByType("text/html")
|
||||
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"html"})
|
||||
|
||||
svConfig := b.H.Sites[1].conf
|
||||
f, _ := svConfig.OutputFormats.Config.GetByName("html")
|
||||
b.Assert(f.Path, qt.Equals, "foo")
|
||||
m, _ = svConfig.MediaTypes.Config.GetByType("text/html")
|
||||
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"bar"})
|
||||
|
||||
}
|
||||
|
||||
func TestConfigMiscPanics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1253,3 +1295,242 @@ Home.
|
||||
b.Assert(b.H.Configs.LanguageConfigSlice[0].Module.Mounts, qt.HasLen, 7)
|
||||
|
||||
}
|
||||
|
||||
func TestDefaultContentLanguageInSubdirOnlyOneLanguage(t *testing.T) {
|
||||
|
||||
t.Run("One language, default in sub dir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
disableKinds = ["taxonomy", "term", "page", "section"]
|
||||
-- content/foo/bar.txt --
|
||||
Foo.
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
`
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/en/index.html", "Home.")
|
||||
b.AssertFileContent("public/en/foo/bar.txt", "Foo.")
|
||||
b.AssertFileContent("public/index.html", "refresh")
|
||||
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
|
||||
b.AssertFileContent("public/en/sitemap.xml", "urlset")
|
||||
})
|
||||
|
||||
t.Run("Two languages, default in sub dir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = true
|
||||
disableKinds = ["taxonomy", "term", "page", "section"]
|
||||
[languages]
|
||||
[languages.en]
|
||||
title = "English Title"
|
||||
[languages.sv]
|
||||
title = "Swedish Title"
|
||||
-- content/foo/bar.txt --
|
||||
Foo.
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
`
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/en/index.html", "Home.")
|
||||
b.AssertFileContent("public/en/foo/bar.txt", "Foo.")
|
||||
b.AssertFileContent("public/index.html", "refresh")
|
||||
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
|
||||
b.AssertFileContent("public/en/sitemap.xml", "urlset")
|
||||
})
|
||||
|
||||
t.Run("Two languages, default in root", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
defaultContentLanguage = "en"
|
||||
defaultContentLanguageInSubdir = false
|
||||
disableKinds = ["taxonomy", "term", "page", "section"]
|
||||
[languages]
|
||||
[languages.en]
|
||||
title = "English Title"
|
||||
[languages.sv]
|
||||
title = "Swedish Title"
|
||||
-- content/foo/bar.txt --
|
||||
Foo.
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
`
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "Home.")
|
||||
b.AssertFileContent("public/foo/bar.txt", "Foo.")
|
||||
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
|
||||
b.AssertFileContent("public/en/sitemap.xml", "urlset")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestLanguagesDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[languages]
|
||||
[languages.en]
|
||||
title = "English Title"
|
||||
[languages.sv]
|
||||
title = "Swedish Title"
|
||||
disabled = true
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
|
||||
|
||||
`
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.Assert(len(b.H.Sites), qt.Equals, 1)
|
||||
|
||||
}
|
||||
|
||||
func TestLoadConfigYamlEnvVar(t *testing.T) {
|
||||
|
||||
defaultEnv := []string{`HUGO_OUTPUTS=home: ['json']`}
|
||||
|
||||
runVariant := func(t testing.TB, files string, env []string) *IntegrationTestBuilder {
|
||||
if env == nil {
|
||||
env = defaultEnv
|
||||
}
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Environ: env,
|
||||
BuildCfg: BuildCfg{SkipRender: true},
|
||||
},
|
||||
).Build()
|
||||
|
||||
outputs := b.H.Configs.Base.Outputs
|
||||
if env == nil {
|
||||
home := outputs["home"]
|
||||
b.Assert(home, qt.Not(qt.IsNil))
|
||||
b.Assert(home, qt.DeepEquals, []string{"json"})
|
||||
}
|
||||
|
||||
return b
|
||||
|
||||
}
|
||||
|
||||
t.Run("with empty slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
[outputs]
|
||||
home = ["html"]
|
||||
|
||||
`
|
||||
b := runVariant(t, files, []string{`HUGO_OUTPUTS=section: []`})
|
||||
outputs := b.H.Configs.Base.Outputs
|
||||
b.Assert(outputs, qt.DeepEquals, map[string][]string{
|
||||
"home": {"html"},
|
||||
"page": {"html"},
|
||||
"rss": {"rss"},
|
||||
"section": nil,
|
||||
"taxonomy": {"html", "rss"},
|
||||
"term": {"html", "rss"},
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("with existing outputs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
[outputs]
|
||||
home = ["html"]
|
||||
|
||||
`
|
||||
|
||||
runVariant(t, files, nil)
|
||||
|
||||
})
|
||||
|
||||
{
|
||||
t.Run("with existing outputs direct", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
[outputs]
|
||||
home = ["html"]
|
||||
|
||||
`
|
||||
runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("without existing outputs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
|
||||
`
|
||||
|
||||
runVariant(t, files, nil)
|
||||
|
||||
})
|
||||
|
||||
t.Run("without existing outputs direct", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.com"
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
`
|
||||
|
||||
runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -680,7 +680,14 @@ func (m *contentMap) splitKey(k string) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(k, "/")[1:]
|
||||
parts := strings.Split(k, "/")[1:]
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
if parts[len(parts)-1] == "" {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (m *contentMap) testDump() string {
|
||||
|
||||
@@ -1178,3 +1178,32 @@ target = "content/resources-b"
|
||||
b.AssertFileContent("public/resources-a/subdir/about/index.html", "Single")
|
||||
b.AssertFileContent("public/resources-b/subdir/about/index.html", "Single")
|
||||
}
|
||||
|
||||
func TestMountData(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'https://example.org/'
|
||||
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
|
||||
|
||||
[[module.mounts]]
|
||||
source = "data"
|
||||
target = "data"
|
||||
|
||||
[[module.mounts]]
|
||||
source = "extra-data"
|
||||
target = "data/extra"
|
||||
-- extra-data/test.yaml --
|
||||
message: Hugo Rocks
|
||||
-- layouts/index.html --
|
||||
{{ site.Data.extra.test.message }}
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "Hugo Rocks")
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@ type HugoSites struct {
|
||||
contentInit sync.Once
|
||||
content *pageMaps
|
||||
|
||||
postRenderInit sync.Once
|
||||
|
||||
// Keeps track of bundle directories and symlinks to enable partial rebuilding.
|
||||
ContentChanges *contentChangeMap
|
||||
|
||||
@@ -435,7 +437,7 @@ func (cfg *BuildCfg) shouldRender(p *pageState) bool {
|
||||
}
|
||||
|
||||
func (h *HugoSites) renderCrossSitesSitemap() error {
|
||||
if !h.isMultiLingual() || h.Conf.IsMultihost() {
|
||||
if h.Conf.IsMultihost() || !(h.Conf.DefaultContentLanguageInSubdir() || h.Conf.IsMultiLingual()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -553,13 +555,14 @@ func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
|
||||
|
||||
h.data = make(map[string]any)
|
||||
for _, fi := range fis {
|
||||
basePath := fi.Meta().Path
|
||||
fileSystem := spec.NewFilesystemFromFileMetaInfo(fi)
|
||||
files, err := fileSystem.Files()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range files {
|
||||
if err := h.handleDataFile(r); err != nil {
|
||||
if err := h.handleDataFile(basePath, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -568,7 +571,7 @@ func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (h *HugoSites) handleDataFile(r source.File) error {
|
||||
func (h *HugoSites) handleDataFile(basePath string, r source.File) error {
|
||||
var current map[string]any
|
||||
|
||||
f, err := r.FileInfo().Meta().Open()
|
||||
@@ -579,7 +582,8 @@ func (h *HugoSites) handleDataFile(r source.File) error {
|
||||
|
||||
// Crawl in data tree to insert data
|
||||
current = h.data
|
||||
keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
|
||||
dataPath := filepath.Join(basePath, r.Dir())
|
||||
keyParts := strings.Split(dataPath, helpers.FilePathSeparator)
|
||||
|
||||
for _, key := range keyParts {
|
||||
if key != "" {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/bep/logg"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/publisher"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
|
||||
@@ -143,6 +144,11 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
|
||||
if err := h.render(infol, conf); err != nil {
|
||||
h.SendError(fmt.Errorf("render: %w", err))
|
||||
}
|
||||
|
||||
if err := h.postRenderOnce(); err != nil {
|
||||
h.SendError(fmt.Errorf("postRenderOnce: %w", err))
|
||||
}
|
||||
|
||||
if err := h.postProcess(infol); err != nil {
|
||||
h.SendError(fmt.Errorf("postProcess: %w", err))
|
||||
}
|
||||
@@ -314,6 +320,34 @@ func (h *HugoSites) render(l logg.LevelLogger, config *BuildCfg) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HugoSites) postRenderOnce() error {
|
||||
h.postRenderInit.Do(func() {
|
||||
conf := h.Configs.Base
|
||||
if conf.PrintPathWarnings {
|
||||
// We need to do this before any post processing, as that may write to the same files twice
|
||||
// and create false positives.
|
||||
hugofs.WalkFilesystems(h.Fs.PublishDir, func(fs afero.Fs) bool {
|
||||
if dfs, ok := fs.(hugofs.DuplicatesReporter); ok {
|
||||
dupes := dfs.ReportDuplicates()
|
||||
if dupes != "" {
|
||||
h.Log.Warnln("Duplicate target paths:", dupes)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if conf.PrintUnusedTemplates {
|
||||
unusedTemplates := h.Tmpl().(tpl.UnusedTemplatesProvider).UnusedTemplates()
|
||||
for _, unusedTemplate := range unusedTemplates {
|
||||
h.Log.Warnf("Template %s is unused, source file %s", unusedTemplate.Name(), unusedTemplate.Filename())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HugoSites) postProcess(l logg.LevelLogger) error {
|
||||
defer h.timeTrack(l, time.Now(), "postProcess")
|
||||
|
||||
@@ -441,7 +475,7 @@ func (h *HugoSites) writeBuildStats() error {
|
||||
if h.ResourceSpec == nil {
|
||||
panic("h.ResourceSpec is nil")
|
||||
}
|
||||
if !h.ResourceSpec.BuildConfig().WriteStats {
|
||||
if !h.ResourceSpec.BuildConfig().BuildStats.Enabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,15 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
|
||||
if match == "" || strings.HasPrefix(match, "#") {
|
||||
continue
|
||||
}
|
||||
var negate bool
|
||||
if strings.HasPrefix(match, "! ") {
|
||||
negate = true
|
||||
match = strings.TrimPrefix(match, "! ")
|
||||
}
|
||||
if negate {
|
||||
s.Assert(content, qt.Not(qt.Contains), match, qt.Commentf(m))
|
||||
continue
|
||||
}
|
||||
s.Assert(content, qt.Contains, match, qt.Commentf(m))
|
||||
}
|
||||
}
|
||||
@@ -205,6 +214,10 @@ func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) LogString() string {
|
||||
return s.logBuff.String()
|
||||
}
|
||||
|
||||
func (s *IntegrationTestBuilder) BuildE() (*IntegrationTestBuilder, error) {
|
||||
s.Helper()
|
||||
if err := s.initBuilder(); err != nil {
|
||||
@@ -381,7 +394,8 @@ func (s *IntegrationTestBuilder) initBuilder() error {
|
||||
s.Assert(os.Chdir(s.Cfg.WorkingDir), qt.IsNil)
|
||||
s.C.Cleanup(func() { os.Chdir(wd) })
|
||||
sc := security.DefaultConfig
|
||||
sc.Exec.Allow = security.NewWhitelist("npm")
|
||||
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
||||
s.Assert(err, qt.IsNil)
|
||||
ex := hexec.New(sc)
|
||||
command, err := ex.New("npm", "install")
|
||||
s.Assert(err, qt.IsNil)
|
||||
|
||||
+21
-14
@@ -18,6 +18,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
)
|
||||
@@ -108,14 +109,16 @@ func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.Target
|
||||
)
|
||||
|
||||
d := s.Deps
|
||||
var classifier files.ContentClass
|
||||
|
||||
if !p.File().IsZero() {
|
||||
dir = p.File().Dir()
|
||||
baseName = p.File().TranslationBaseName()
|
||||
contentBaseName = p.File().ContentBaseName()
|
||||
classifier = p.File().FileInfo().Meta().Classifier
|
||||
}
|
||||
|
||||
if baseName != contentBaseName {
|
||||
if classifier == files.ContentClassLeaf {
|
||||
// See https://github.com/gohugoio/hugo/issues/4870
|
||||
// A leaf bundle
|
||||
dir = strings.TrimSuffix(dir, contentBaseName+helpers.FilePathSeparator)
|
||||
@@ -143,22 +146,26 @@ func createTargetPathDescriptor(s *Site, p page.Page, pm *pageMeta) (page.Target
|
||||
desc.PrefixFilePath = s.getLanguageTargetPathLang(alwaysInSubDir)
|
||||
desc.PrefixLink = s.getLanguagePermalinkLang(alwaysInSubDir)
|
||||
|
||||
// Expand only page.KindPage and page.KindTaxonomy; don't expand other Kinds of Pages
|
||||
// like page.KindSection or page.KindTaxonomyTerm because they are "shallower" and
|
||||
// the permalink configuration values are likely to be redundant, e.g.
|
||||
// naively expanding /category/:slug/ would give /category/categories/ for
|
||||
// the "categories" page.KindTaxonomyTerm.
|
||||
if p.Kind() == page.KindPage || p.Kind() == page.KindTerm {
|
||||
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
opath, err := d.ResourceSpec.Permalinks.Expand(p.Section(), p)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
}
|
||||
|
||||
if opath != "" {
|
||||
opath, _ = url.QueryUnescape(opath)
|
||||
if strings.HasSuffix(opath, "//") {
|
||||
// When rewriting the _index of the section the permalink config is applied to,
|
||||
// we get douple slashes at the end sometimes; clear them up here
|
||||
opath = strings.TrimSuffix(opath, "/")
|
||||
}
|
||||
|
||||
if opath != "" {
|
||||
opath, _ = url.QueryUnescape(opath)
|
||||
desc.ExpandedPermalink = opath
|
||||
}
|
||||
desc.ExpandedPermalink = opath
|
||||
|
||||
if !p.File().IsZero() {
|
||||
s.Log.Debugf("Set expanded permalink path for %s %s to %#v", p.Kind(), p.File().Path(), opath)
|
||||
} else {
|
||||
s.Log.Debugf("Set expanded permalink path for %s in %v to %#v", p.Kind(), desc.Sections, opath)
|
||||
}
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
|
||||
@@ -557,6 +557,9 @@ func (p *pageContentOutput) RenderWithTemplateInfo(ctx context.Context, info tpl
|
||||
}
|
||||
|
||||
func (p *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
|
||||
if len(layout) == 0 {
|
||||
return "", errors.New("no layout given")
|
||||
}
|
||||
templ, found, err := p.p.resolveTemplate(layout...)
|
||||
if err != nil {
|
||||
return "", p.p.wrapError(err)
|
||||
|
||||
+25
-3
@@ -22,7 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bep/clock"
|
||||
"github.com/bep/clocks"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/markup/asciidocext"
|
||||
"github.com/gohugoio/hugo/markup/rst"
|
||||
@@ -1502,8 +1502,8 @@ func TestShouldBuild(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestShouldBuildWithClock(t *testing.T) {
|
||||
htime.Clock = clock.Start(time.Date(2021, 11, 17, 20, 34, 58, 651387237, time.UTC))
|
||||
t.Cleanup(func() { htime.Clock = clock.System() })
|
||||
htime.Clock = clocks.Start(time.Date(2021, 11, 17, 20, 34, 58, 651387237, time.UTC))
|
||||
t.Cleanup(func() { htime.Clock = clocks.System() })
|
||||
past := time.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
future := time.Date(2037, 11, 17, 20, 34, 58, 651387237, time.UTC)
|
||||
zero := time.Time{}
|
||||
@@ -1983,3 +1983,25 @@ title: "p2"
|
||||
b.Assert(identity.HashString(p1), qt.Not(qt.Equals), identity.HashString(p2))
|
||||
b.Assert(identity.HashString(sites[0]), qt.Not(qt.Equals), identity.HashString(sites[1]))
|
||||
}
|
||||
|
||||
// Issue #11243
|
||||
func TestRenderWithoutArgument(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/index.html --
|
||||
{{ .Render }}
|
||||
`
|
||||
|
||||
b, err := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
Running: true,
|
||||
},
|
||||
).BuildE()
|
||||
|
||||
b.Assert(err, qt.IsNotNil)
|
||||
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
|
||||
fs, cfg := newTestBundleSources(c)
|
||||
cfg.Set("baseURL", baseURL)
|
||||
cfg.Set("canonifyURLs", canonify)
|
||||
cfg.Set("defaultContentLanguageInSubdir", false)
|
||||
|
||||
cfg.Set("permalinks", map[string]string{
|
||||
"a": ":sections/:filename",
|
||||
|
||||
+2
-35
@@ -103,48 +103,15 @@ func (p *Paths) Lang() string {
|
||||
}
|
||||
|
||||
func (p *Paths) GetTargetLanguageBasePath() string {
|
||||
if len(p.Cfg.Languages()) > 1 {
|
||||
if p.Cfg.IsMultihost() {
|
||||
// In a multihost configuration all assets will be published below the language code.
|
||||
return p.Lang()
|
||||
}
|
||||
return p.GetLanguagePrefix()
|
||||
}
|
||||
|
||||
func (p *Paths) GetURLLanguageBasePath() string {
|
||||
if len(p.Cfg.Languages()) > 1 {
|
||||
return ""
|
||||
}
|
||||
return p.GetLanguagePrefix()
|
||||
}
|
||||
|
||||
func (p *Paths) GetLanguagePrefix() string {
|
||||
if len(p.Cfg.Languages()) < 2 {
|
||||
return ""
|
||||
}
|
||||
defaultLang := p.Cfg.DefaultContentLanguage()
|
||||
defaultInSubDir := p.Cfg.DefaultContentLanguageInSubdir()
|
||||
currentLang := p.Cfg.Language().Lang
|
||||
if currentLang == "" || (currentLang == defaultLang && !defaultInSubDir) {
|
||||
return ""
|
||||
}
|
||||
return currentLang
|
||||
}
|
||||
|
||||
// GetLangSubDir returns the given language's subdir if needed.
|
||||
func (p *Paths) GetLangSubDir(lang string) string {
|
||||
if len(p.Cfg.Languages()) < 2 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if p.Cfg.IsMultihost() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lang == "" || (lang == p.Cfg.DefaultContentLanguage() && !p.Cfg.DefaultContentLanguageInSubdir()) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return lang
|
||||
return p.Cfg.LanguagePrefix()
|
||||
}
|
||||
|
||||
// AbsPathify creates an absolute path if given a relative path. If already
|
||||
|
||||
+3
-8
@@ -887,21 +887,16 @@ func (s *Site) getLanguageTargetPathLang(alwaysInSubDir bool) string {
|
||||
|
||||
// get any language code to prefix the relative permalink with.
|
||||
func (s *Site) getLanguagePermalinkLang(alwaysInSubDir bool) string {
|
||||
if !s.h.isMultiLingual() || s.h.Conf.IsMultihost() {
|
||||
if s.h.Conf.IsMultihost() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if alwaysInSubDir {
|
||||
if s.h.Conf.IsMultiLingual() && alwaysInSubDir {
|
||||
return s.Language().Lang
|
||||
}
|
||||
|
||||
isDefault := s.Language().Lang == s.conf.DefaultContentLanguage
|
||||
return s.GetLanguagePrefix()
|
||||
|
||||
if !isDefault || s.conf.DefaultContentLanguageInSubdir {
|
||||
return s.Language().Lang
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Site) getTaxonomyKey(key string) string {
|
||||
|
||||
+3
-8
@@ -470,16 +470,11 @@ func (s *Site) IsMultiLingual() bool {
|
||||
}
|
||||
|
||||
func (s *Site) LanguagePrefix() string {
|
||||
conf := s.s.Conf
|
||||
if !conf.IsMultiLingual() {
|
||||
prefix := s.GetLanguagePrefix()
|
||||
if prefix == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !conf.DefaultContentLanguageInSubdir() && s.language.Lang == conf.DefaultContentLanguage() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "/" + s.language.Lang
|
||||
return "/" + prefix
|
||||
}
|
||||
|
||||
// Returns the identity of this site.
|
||||
|
||||
@@ -384,7 +384,7 @@ func (s *Site) renderAliases() error {
|
||||
// renderMainLanguageRedirect creates a redirect to the main language home,
|
||||
// depending on if it lives in sub folder (e.g. /en) or not.
|
||||
func (s *Site) renderMainLanguageRedirect() error {
|
||||
if !s.h.isMultiLingual() || s.h.Conf.IsMultihost() {
|
||||
if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultiLingual()) {
|
||||
// No need for a redirect
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1162,6 +1162,91 @@ Some text.
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassCollectorConfigWriteStats(t *testing.T) {
|
||||
r := func(writeStatsConfig string) *IntegrationTestBuilder {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
WRITE_STATS_CONFIG
|
||||
-- layouts/_default/list.html --
|
||||
<div id="myid" class="myclass">Foo</div>
|
||||
|
||||
`
|
||||
files = strings.Replace(files, "WRITE_STATS_CONFIG", writeStatsConfig, 1)
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
NeedsOsFS: true,
|
||||
},
|
||||
).Build()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Legacy config.
|
||||
b := r(`
|
||||
[build]
|
||||
writeStats = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
|
||||
|
||||
b = r(`
|
||||
[build]
|
||||
writeStats = false
|
||||
`)
|
||||
|
||||
b.AssertDestinationExists("hugo_stats.json", false)
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = true
|
||||
disableids = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "myclass", "div", "! myid")
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = true
|
||||
disableclasses = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "! myclass", "div", "myid")
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = true
|
||||
disabletags = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "myclass", "! div", "myid")
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = true
|
||||
disabletags = true
|
||||
disableclasses = true
|
||||
`)
|
||||
|
||||
b.AssertFileContent("hugo_stats.json", "! myclass", "! div", "myid")
|
||||
|
||||
b = r(`
|
||||
[build.buildStats]
|
||||
enable = false
|
||||
`)
|
||||
b.AssertDestinationExists("hugo_stats.json", false)
|
||||
|
||||
}
|
||||
|
||||
func TestClassCollectorStress(t *testing.T) {
|
||||
statsFilename := "hugo_stats.json"
|
||||
defer os.Remove(statsFilename)
|
||||
|
||||
+22
-29
@@ -38,35 +38,6 @@ var urlFakeSource = [][2]string{
|
||||
{filepath.FromSlash("content/blue/doc2.md"), slugDoc2},
|
||||
}
|
||||
|
||||
func TestPageCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
cfg, fs := newTestCfg()
|
||||
cfg.Set("uglyURLs", false)
|
||||
cfg.Set("paginate", 10)
|
||||
configs, err := loadTestConfigFromProvider(cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
writeSourcesToSource(t, "", fs, urlFakeSource...)
|
||||
s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Configs: configs}, BuildCfg{})
|
||||
|
||||
_, err = s.Fs.WorkingDirReadOnly.Open("public/blue")
|
||||
if err != nil {
|
||||
t.Errorf("No indexed rendered.")
|
||||
}
|
||||
|
||||
for _, pth := range []string{
|
||||
"public/sd1/foo/index.html",
|
||||
"public/sd2/index.html",
|
||||
"public/sd3/index.html",
|
||||
"public/sd4.html",
|
||||
} {
|
||||
if _, err := s.Fs.WorkingDirReadOnly.Open(filepath.FromSlash(pth)); err != nil {
|
||||
t.Errorf("No alias rendered: %s", pth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUglyURLsPerSection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -160,3 +131,25 @@ Do not go gentle into that good night.
|
||||
th.assertFileContent(filepath.Join("public", "ss1", "index.html"), "P1|URL: /ss1/|Next: /ss1/page/2/")
|
||||
th.assertFileContent(filepath.Join("public", "ss1", "page", "2", "index.html"), "P2|URL: /ss1/page/2/|Next: /ss1/page/3/")
|
||||
}
|
||||
|
||||
func TestSectionsEntries(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- content/withfile/_index.md --
|
||||
-- content/withoutfile/p1.md --
|
||||
-- layouts/_default/list.html --
|
||||
SectionsEntries: {{ .SectionsEntries }}
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := NewIntegrationTestBuilder(
|
||||
IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/withfile/index.html", "SectionsEntries: [withfile]")
|
||||
b.AssertFileContent("public/withoutfile/index.html", "SectionsEntries: [withoutfile]")
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) {
|
||||
c := qt.New(t)
|
||||
cfg, fs := newTestCfg()
|
||||
cfg.Set("baseURL", "http://auth/bub/")
|
||||
cfg.Set("defaultContentLanguageInSubdir", false)
|
||||
configs, err := loadTestConfigFromProvider(cfg)
|
||||
c.Assert(err, qt.IsNil)
|
||||
writeSource(t, fs, "layouts/sitemap.xml", sitemapTemplate)
|
||||
|
||||
@@ -834,7 +834,9 @@ func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page {
|
||||
|
||||
func (s *sitesBuilder) NpmInstall() hexec.Runner {
|
||||
sc := security.DefaultConfig
|
||||
sc.Exec.Allow = security.NewWhitelist("npm")
|
||||
var err error
|
||||
sc.Exec.Allow, err = security.NewWhitelist("npm")
|
||||
s.Assert(err, qt.IsNil)
|
||||
ex := hexec.New(sc)
|
||||
command, err := ex.New("npm", "install")
|
||||
s.Assert(err, qt.IsNil)
|
||||
@@ -935,8 +937,7 @@ func newTestCfgBasic() (config.Provider, *hugofs.Fs) {
|
||||
func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) {
|
||||
mm := afero.NewMemMapFs()
|
||||
cfg := config.New()
|
||||
// Default is false, but true is easier to use as default in tests
|
||||
cfg.Set("defaultContentLanguageInSubdir", true)
|
||||
cfg.Set("defaultContentLanguageInSubdir", false)
|
||||
cfg.Set("publishDir", "public")
|
||||
|
||||
fs := hugofs.NewFromOld(hugofs.NewBaseFileDecorator(mm), cfg)
|
||||
|
||||
+7
-2
@@ -1,7 +1,12 @@
|
||||
# Release env.
|
||||
# These will be replaced by script before release.
|
||||
HUGORELEASER_TAG=v0.114.0
|
||||
HUGORELEASER_COMMITISH=9df2ec7988e5a217a14901cc76c0b7e76b2e9f02
|
||||
HUGORELEASER_TAG=v0.115.3
|
||||
HUGORELEASER_COMMITISH=5c2e014a5150553a9fa4f9c1eb7dc4db89c0f1ab
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ type LanguageConfig struct {
|
||||
// The language weight. When set to a non-zero value, this will
|
||||
// be the main sort criteria for the language.
|
||||
Weight int
|
||||
|
||||
// Set to true to disable this language.
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
func DecodeConfig(m map[string]any) (map[string]LanguageConfig, error) {
|
||||
|
||||
+1
-1
@@ -119,7 +119,7 @@ func (t Translator) initFuncs(bndl *i18n.Bundle) {
|
||||
t.logger.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err)
|
||||
}
|
||||
|
||||
if t.cfg.LogI18nWarnings() {
|
||||
if t.cfg.PrintI18nWarnings() {
|
||||
t.logger.Warnf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID)
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestUnfinished(t *testing.T) {
|
||||
|
||||
p := commonTestScriptsParam
|
||||
p.Dir = "testscripts/unfinished"
|
||||
//p.UpdateScripts = true
|
||||
|
||||
testscript.Run(t, p)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ func TestConvert(t *testing.T) {
|
||||
}
|
||||
c := qt.New(t)
|
||||
sc := security.DefaultConfig
|
||||
sc.Exec.Allow = security.NewWhitelist("pandoc")
|
||||
var err error
|
||||
sc.Exec.Allow, err = security.NewWhitelist("pandoc")
|
||||
c.Assert(err, qt.IsNil)
|
||||
p, err := Provider.New(converter.ProviderConfig{Exec: hexec.New(sc), Logger: loggers.NewDefault()})
|
||||
c.Assert(err, qt.IsNil)
|
||||
conv, err := p.New(converter.DocumentContext{})
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestConvert(t *testing.T) {
|
||||
}
|
||||
c := qt.New(t)
|
||||
sc := security.DefaultConfig
|
||||
sc.Exec.Allow = security.NewWhitelist("rst", "python")
|
||||
sc.Exec.Allow = security.MustNewWhitelist("rst", "python")
|
||||
|
||||
p, err := Provider.New(
|
||||
converter.ProviderConfig{
|
||||
|
||||
+3
-1
@@ -292,8 +292,10 @@ func (c *Client) Vendor() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Also include any theme.toml or config.* files in the root.
|
||||
// Also include any theme.toml or config.* or hugo.* files in the root.
|
||||
configFiles, _ := afero.Glob(c.fs, filepath.Join(dir, "config.*"))
|
||||
configFiles2, _ := afero.Glob(c.fs, filepath.Join(dir, "hugo.*"))
|
||||
configFiles = append(configFiles, configFiles2...)
|
||||
configFiles = append(configFiles, filepath.Join(dir, "theme.toml"))
|
||||
for _, configFile := range configFiles {
|
||||
if err := hugio.CopyFile(c.fs, configFile, filepath.Join(vendorDir, t.Path(), filepath.Base(configFile))); err != nil {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/niklasfasching/go-org/org"
|
||||
|
||||
xml "github.com/clbanning/mxj/v2"
|
||||
@@ -90,7 +91,7 @@ func (d Decoder) UnmarshalStringTo(data string, typ any) (any, error) {
|
||||
switch typ.(type) {
|
||||
case string:
|
||||
return data, nil
|
||||
case map[string]any:
|
||||
case map[string]any, maps.Params:
|
||||
format := d.FormatFromContentString(data)
|
||||
return d.UnmarshalToMap([]byte(data), format)
|
||||
case []any:
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
)
|
||||
|
||||
@@ -46,8 +47,9 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newHTMLElementsCollector() *htmlElementsCollector {
|
||||
func newHTMLElementsCollector(conf config.BuildStats) *htmlElementsCollector {
|
||||
return &htmlElementsCollector{
|
||||
conf: conf,
|
||||
elementSet: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
@@ -93,6 +95,8 @@ type htmlElement struct {
|
||||
}
|
||||
|
||||
type htmlElementsCollector struct {
|
||||
conf config.BuildStats
|
||||
|
||||
// Contains the raw HTML string. We will get the same element
|
||||
// several times, and want to avoid costly reparsing when this
|
||||
// is used for aggregated data only.
|
||||
@@ -113,7 +117,9 @@ func (c *htmlElementsCollector) getHTMLElements() HTMLElements {
|
||||
for _, el := range c.elements {
|
||||
classes = append(classes, el.Classes...)
|
||||
ids = append(ids, el.IDs...)
|
||||
tags = append(tags, el.Tag)
|
||||
if !c.conf.DisableTags {
|
||||
tags = append(tags, el.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
classes = helpers.UniqueStringsSorted(classes)
|
||||
@@ -246,7 +252,7 @@ func (w *htmlElementsCollectorWriter) lexElementInside(resolve htmlCollectorStat
|
||||
}
|
||||
|
||||
// Parse each collected element.
|
||||
el, err := parseHTMLElement(s)
|
||||
el, err := w.parseHTMLElement(s)
|
||||
if err != nil {
|
||||
w.err = err
|
||||
return resolve
|
||||
@@ -363,7 +369,8 @@ func htmlLexToEndOfComment(w *htmlElementsCollectorWriter) htmlCollectorStateFun
|
||||
return htmlLexToEndOfComment
|
||||
}
|
||||
|
||||
func parseHTMLElement(elStr string) (el htmlElement, err error) {
|
||||
func (w *htmlElementsCollectorWriter) parseHTMLElement(elStr string) (el htmlElement, err error) {
|
||||
conf := w.collector.conf
|
||||
|
||||
tagName := parseStartTag(elStr)
|
||||
|
||||
@@ -390,8 +397,13 @@ func parseHTMLElement(elStr string) (el htmlElement, err error) {
|
||||
switch {
|
||||
case strings.EqualFold(a.Key, "id"):
|
||||
// There should be only one, but one never knows...
|
||||
el.IDs = append(el.IDs, a.Val)
|
||||
if !conf.DisableIDs {
|
||||
el.IDs = append(el.IDs, a.Val)
|
||||
}
|
||||
default:
|
||||
if conf.DisableClasses {
|
||||
continue
|
||||
}
|
||||
if classAttrRe.MatchString(a.Key) {
|
||||
el.Classes = append(el.Classes, strings.Fields(a.Val)...)
|
||||
} else {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/testconfig"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/minifiers"
|
||||
@@ -136,7 +137,9 @@ func TestClassCollector(t *testing.T) {
|
||||
} {
|
||||
|
||||
c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) {
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
|
||||
config.BuildStats{Enable: true},
|
||||
))
|
||||
if variant.minify {
|
||||
if skipMinifyTest[test.name] {
|
||||
c.Skip("skip minify test")
|
||||
@@ -240,7 +243,9 @@ func BenchmarkElementsCollectorWriter(b *testing.B) {
|
||||
</html>
|
||||
`
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
|
||||
config.BuildStats{Enable: true},
|
||||
))
|
||||
fmt.Fprint(w, benchHTML)
|
||||
|
||||
}
|
||||
@@ -262,7 +267,9 @@ func BenchmarkElementsCollectorWriterPre(b *testing.B) {
|
||||
<div class="foo"></div>
|
||||
|
||||
`
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector())
|
||||
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
|
||||
config.BuildStats{Enable: true},
|
||||
))
|
||||
for i := 0; i < b.N; i++ {
|
||||
fmt.Fprint(w, benchHTML)
|
||||
|
||||
|
||||
@@ -81,8 +81,8 @@ func NewDestinationPublisher(rs *resources.Spec, outputFormats output.Formats, m
|
||||
fs := rs.BaseFs.PublishFs
|
||||
cfg := rs.Cfg
|
||||
var classCollector *htmlElementsCollector
|
||||
if rs.BuildConfig().WriteStats {
|
||||
classCollector = newHTMLElementsCollector()
|
||||
if rs.BuildConfig().BuildStats.Enabled() {
|
||||
classCollector = newHTMLElementsCollector(rs.BuildConfig().BuildStats)
|
||||
}
|
||||
pub = DestinationPublisher{fs: fs, htmlElementsCollector: classCollector}
|
||||
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
|
||||
|
||||
@@ -221,7 +221,7 @@ func generateFileIsZeroWrappers(c *codegen.Inspector) error {
|
||||
methods := c.MethodsFromTypes([]reflect.Type{reflect.TypeOf((*source.File)(nil)).Elem()}, nil)
|
||||
|
||||
for _, m := range methods {
|
||||
if m.Name == "IsZero" {
|
||||
if m.Name == "IsZero" || m.Name == "Classifier" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprint(&buff, m.DeclarationNamed("zeroFile"))
|
||||
@@ -255,6 +255,11 @@ func (zeroFile) IsZero() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (z zeroFile) Classifier() files.ContentClass {
|
||||
z.log.Warnln(".File.Classifier on zero object. Wrap it in if or with: {{ with .File }}{{ .Classifier }}{{ end }}")
|
||||
return files.ContentClassZero
|
||||
}
|
||||
|
||||
%s
|
||||
|
||||
`, header, importsString(pkgImports), buff.String())
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ type PermalinkExpander struct {
|
||||
// to be used to replace that tag.
|
||||
knownPermalinkAttributes map[string]pageToPermaAttribute
|
||||
|
||||
expanders map[string]func(Page) (string, error)
|
||||
expanders map[string]map[string]func(Page) (string, error)
|
||||
|
||||
urlize func(uri string) string
|
||||
}
|
||||
@@ -68,7 +69,7 @@ func (p PermalinkExpander) callback(attr string) (pageToPermaAttribute, bool) {
|
||||
|
||||
// NewPermalinkExpander creates a new PermalinkExpander configured by the given
|
||||
// urlize func.
|
||||
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]string) (PermalinkExpander, error) {
|
||||
func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]map[string]string) (PermalinkExpander, error) {
|
||||
p := PermalinkExpander{urlize: urlize}
|
||||
|
||||
p.knownPermalinkAttributes = map[string]pageToPermaAttribute{
|
||||
@@ -87,12 +88,15 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]st
|
||||
"filename": p.pageToPermalinkFilename,
|
||||
}
|
||||
|
||||
e, err := p.parse(patterns)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.expanders = make(map[string]map[string]func(Page) (string, error))
|
||||
|
||||
p.expanders = e
|
||||
for kind, patterns := range patterns {
|
||||
e, err := p.parse(patterns)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.expanders[kind] = e
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
@@ -100,7 +104,13 @@ func NewPermalinkExpander(urlize func(uri string) string, patterns map[string]st
|
||||
// Expand expands the path in p according to the rules defined for the given key.
|
||||
// If no rules are found for the given key, an empty string is returned.
|
||||
func (l PermalinkExpander) Expand(key string, p Page) (string, error) {
|
||||
expand, found := l.expanders[key]
|
||||
expanders, found := l.expanders[p.Kind()]
|
||||
|
||||
if !found {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
expand, found := expanders[key]
|
||||
|
||||
if !found {
|
||||
return "", nil
|
||||
@@ -247,11 +257,13 @@ func (l PermalinkExpander) pageToPermalinkTitle(p Page, _ string) (string, error
|
||||
|
||||
// pageToPermalinkFilename returns the URL-safe form of the filename
|
||||
func (l PermalinkExpander) pageToPermalinkFilename(p Page, _ string) (string, error) {
|
||||
name := p.File().TranslationBaseName()
|
||||
name := l.translationBaseName(p)
|
||||
if name == "index" {
|
||||
// Page bundles; the directory name will hopefully have a better name.
|
||||
dir := strings.TrimSuffix(p.File().Dir(), helpers.FilePathSeparator)
|
||||
_, name = filepath.Split(dir)
|
||||
} else if name == "_index" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return l.urlize(name), nil
|
||||
@@ -281,6 +293,13 @@ func (l PermalinkExpander) pageToPermalinkSections(p Page, _ string) (string, er
|
||||
return p.CurrentSection().SectionsPath(), nil
|
||||
}
|
||||
|
||||
func (l PermalinkExpander) translationBaseName(p Page) string {
|
||||
if p.File().IsZero() {
|
||||
return ""
|
||||
}
|
||||
return p.File().TranslationBaseName()
|
||||
}
|
||||
|
||||
var (
|
||||
nilSliceFunc = func(s []string) []string {
|
||||
return nil
|
||||
@@ -333,7 +352,10 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
|
||||
return func(ss []string) int {
|
||||
// Prevent out of bound situations. It would not make
|
||||
// much sense to panic here.
|
||||
if n > len(ss) {
|
||||
if n >= len(ss) {
|
||||
if low {
|
||||
return -1
|
||||
}
|
||||
return len(ss)
|
||||
}
|
||||
return n
|
||||
@@ -349,7 +371,11 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
v := s[toN(s)]
|
||||
n := toN(s)
|
||||
if n < 0 {
|
||||
return []string{}
|
||||
}
|
||||
v := s[n]
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -363,7 +389,59 @@ func (l PermalinkExpander) toSliceFunc(cut string) func(s []string) []string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s[toN1(s):toN2(s)]
|
||||
n1, n2 := toN1(s), toN2(s)
|
||||
if n1 < 0 || n2 < 0 {
|
||||
return []string{}
|
||||
}
|
||||
return s[n1:n2]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var permalinksKindsSuppurt = []string{KindPage, KindSection, KindTaxonomy, KindTerm}
|
||||
|
||||
// DecodePermalinksConfig decodes the permalinks configuration in the given map
|
||||
func DecodePermalinksConfig(m map[string]any) (map[string]map[string]string, error) {
|
||||
permalinksConfig := make(map[string]map[string]string)
|
||||
|
||||
permalinksConfig[KindPage] = make(map[string]string)
|
||||
permalinksConfig[KindSection] = make(map[string]string)
|
||||
permalinksConfig[KindTaxonomy] = make(map[string]string)
|
||||
permalinksConfig[KindTerm] = make(map[string]string)
|
||||
|
||||
config := maps.CleanConfigStringMap(m)
|
||||
for k, v := range config {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
// [permalinks]
|
||||
// key = '...'
|
||||
|
||||
// To sucessfully be backward compatible, "default" patterns need to be set for both page and term
|
||||
permalinksConfig[KindPage][k] = v
|
||||
permalinksConfig[KindTerm][k] = v
|
||||
|
||||
case maps.Params:
|
||||
// [permalinks.key]
|
||||
// xyz = ???
|
||||
|
||||
if helpers.InStringArray(permalinksKindsSuppurt, k) {
|
||||
// TODO: warn if we overwrite an already set value
|
||||
for k2, v2 := range v {
|
||||
switch v2 := v2.(type) {
|
||||
case string:
|
||||
permalinksConfig[k][k2] = v2
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q for kind %q", v2, k2, k)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("permalinks configuration not supported for kind %q, supported kinds are %v", k, permalinksKindsSuppurt)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("permalinks configuration invalid: unknown value %q for key %q", v, k)
|
||||
}
|
||||
}
|
||||
return permalinksConfig, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
// Copyright 2023 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 page_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestPermalinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/_default/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/_default/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.toml --
|
||||
[taxonomies]
|
||||
tag = "tags"
|
||||
[permalinks.page]
|
||||
withpageslug = '/pageslug/:slug/'
|
||||
withallbutlastsection = '/:sections[:last]/:slug/'
|
||||
[permalinks.section]
|
||||
withfilefilename = '/sectionwithfilefilename/:filename/'
|
||||
withfilefiletitle = '/sectionwithfilefiletitle/:title/'
|
||||
withfileslug = '/sectionwithfileslug/:slug/'
|
||||
nofileslug = '/sectionnofileslug/:slug/'
|
||||
nofilefilename = '/sectionnofilefilename/:filename/'
|
||||
nofiletitle1 = '/sectionnofiletitle1/:title/'
|
||||
nofiletitle2 = '/sectionnofiletitle2/:sections[:last]/'
|
||||
[permalinks.term]
|
||||
tags = '/tagsslug/tag/:slug/'
|
||||
[permalinks.taxonomy]
|
||||
tags = '/tagsslug/:slug/'
|
||||
-- content/withpageslug/p1.md --
|
||||
---
|
||||
slug: "p1slugvalue"
|
||||
tags: ["mytag"]
|
||||
---
|
||||
-- content/withfilefilename/_index.md --
|
||||
-- content/withfileslug/_index.md --
|
||||
---
|
||||
slug: "withfileslugvalue"
|
||||
---
|
||||
-- content/nofileslug/p1.md --
|
||||
-- content/nofilefilename/p1.md --
|
||||
-- content/nofiletitle1/p1.md --
|
||||
-- content/nofiletitle2/asdf/p1.md --
|
||||
-- content/withallbutlastsection/subsection/p1.md --
|
||||
-- content/tags/_index.md --
|
||||
---
|
||||
slug: "tagsslug"
|
||||
---
|
||||
-- content/tags/mytag/_index.md --
|
||||
---
|
||||
slug: "mytagslug"
|
||||
---
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
LogLevel: logg.LevelWarn,
|
||||
}).Build()
|
||||
|
||||
t.Log(b.LogString())
|
||||
// No .File.TranslationBaseName on zero object etc. warnings.
|
||||
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||
b.AssertFileContent("public/pageslug/p1slugvalue/index.html", "Single|page|/pageslug/p1slugvalue/|")
|
||||
b.AssertFileContent("public/sectionwithfilefilename/index.html", "List|section|/sectionwithfilefilename/|")
|
||||
b.AssertFileContent("public/sectionwithfileslug/withfileslugvalue/index.html", "List|section|/sectionwithfileslug/withfileslugvalue/|")
|
||||
b.AssertFileContent("public/sectionnofilefilename/index.html", "List|section|/sectionnofilefilename/|")
|
||||
b.AssertFileContent("public/sectionnofileslug/nofileslugs/index.html", "List|section|/sectionnofileslug/nofileslugs/|")
|
||||
b.AssertFileContent("public/sectionnofiletitle1/nofiletitle1s/index.html", "List|section|/sectionnofiletitle1/nofiletitle1s/|")
|
||||
b.AssertFileContent("public/sectionnofiletitle2/index.html", "List|section|/sectionnofiletitle2/|")
|
||||
|
||||
b.AssertFileContent("public/tagsslug/tag/mytagslug/index.html", "List|term|/tagsslug/tag/mytagslug/|")
|
||||
b.AssertFileContent("public/tagsslug/tagsslug/index.html", "List|taxonomy|/tagsslug/tagsslug/|")
|
||||
|
||||
permalinksConf := b.H.Configs.Base.Permalinks
|
||||
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
|
||||
"page": {"withallbutlastsection": "/:sections[:last]/:slug/", "withpageslug": "/pageslug/:slug/"},
|
||||
"section": {"nofilefilename": "/sectionnofilefilename/:filename/", "nofileslug": "/sectionnofileslug/:slug/", "nofiletitle1": "/sectionnofiletitle1/:title/", "nofiletitle2": "/sectionnofiletitle2/:sections[:last]/", "withfilefilename": "/sectionwithfilefilename/:filename/", "withfilefiletitle": "/sectionwithfilefiletitle/:title/", "withfileslug": "/sectionwithfileslug/:slug/"},
|
||||
"taxonomy": {"tags": "/tagsslug/:slug/"},
|
||||
"term": {"tags": "/tagsslug/tag/:slug/"},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestPermalinksOldSetup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- layouts/_default/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/_default/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- hugo.toml --
|
||||
[permalinks]
|
||||
withpageslug = '/pageslug/:slug/'
|
||||
-- content/withpageslug/p1.md --
|
||||
---
|
||||
slug: "p1slugvalue"
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
LogLevel: logg.LevelWarn,
|
||||
}).Build()
|
||||
|
||||
t.Log(b.LogString())
|
||||
// No .File.TranslationBaseName on zero object etc. warnings.
|
||||
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||
b.AssertFileContent("public/pageslug/p1slugvalue/index.html", "Single|page|/pageslug/p1slugvalue/|")
|
||||
|
||||
permalinksConf := b.H.Configs.Base.Permalinks
|
||||
b.Assert(permalinksConf, qt.DeepEquals, map[string]map[string]string{
|
||||
"page": {"withpageslug": "/pageslug/:slug/"},
|
||||
"section": {},
|
||||
"taxonomy": {},
|
||||
"term": {"withpageslug": "/pageslug/:slug/"},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestPermalinksNestedSections(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[permalinks.page]
|
||||
books = '/libros/:sections[1:]/:filename'
|
||||
|
||||
[permalinks.section]
|
||||
books = '/libros/:sections[1:]'
|
||||
-- content/books/_index.md --
|
||||
---
|
||||
title: Books
|
||||
---
|
||||
-- content/books/fiction/_index.md --
|
||||
---
|
||||
title: Fiction
|
||||
---
|
||||
-- content/books/fiction/2023/_index.md --
|
||||
---
|
||||
title: 2023
|
||||
---
|
||||
-- content/books/fiction/2023/book1/index.md --
|
||||
---
|
||||
title: Book 1
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
-- layouts/_default/list.html --
|
||||
List.
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
LogLevel: logg.LevelWarn,
|
||||
}).Build()
|
||||
|
||||
t.Log(b.LogString())
|
||||
// No .File.TranslationBaseName on zero object etc. warnings.
|
||||
b.Assert(b.H.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
|
||||
|
||||
b.AssertFileContent("public/libros/index.html", "List.")
|
||||
b.AssertFileContent("public/libros/fiction/index.html", "List.")
|
||||
b.AssertFileContent("public/libros/fiction/2023/book1/index.html", "Single.")
|
||||
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func TestPermalinkExpansion(t *testing.T) {
|
||||
page.date = d
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
page.kind = "page"
|
||||
|
||||
for _, item := range testdataPermalinks {
|
||||
if !item.valid {
|
||||
@@ -79,8 +80,10 @@ func TestPermalinkExpansion(t *testing.T) {
|
||||
name := specNameCleaner.ReplaceAllString(item.spec, "")
|
||||
|
||||
c.Run(name, func(c *qt.C) {
|
||||
patterns := map[string]string{
|
||||
"posts": item.spec,
|
||||
patterns := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": item.spec,
|
||||
},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, patterns)
|
||||
c.Assert(err, qt.IsNil)
|
||||
@@ -103,14 +106,18 @@ func TestPermalinkExpansionMultiSection(t *testing.T) {
|
||||
page.date = d
|
||||
page.section = "blue"
|
||||
page.slug = "The Slug"
|
||||
page.kind = "page"
|
||||
|
||||
page_slug_fallback := newTestPageWithFile("/page-filename/index.md")
|
||||
page_slug_fallback.title = "Page Title"
|
||||
page_slug_fallback.kind = "page"
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:slug",
|
||||
"blog": "/:section/:year",
|
||||
"recipes": "/:slugorfilename",
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:slug",
|
||||
"blog": "/:section/:year",
|
||||
"recipes": "/:slugorfilename",
|
||||
},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
c.Assert(err, qt.IsNil)
|
||||
@@ -137,8 +144,10 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:slug/",
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:slug/",
|
||||
},
|
||||
}
|
||||
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
@@ -151,6 +160,7 @@ func TestPermalinkExpansionConcurrent(t *testing.T) {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
page := newTestPage()
|
||||
page.kind = "page"
|
||||
for j := 1; j < 20; j++ {
|
||||
page.slug = fmt.Sprintf("slug%d", i+j)
|
||||
expanded, err := expander.Expand("posts", page)
|
||||
@@ -169,36 +179,45 @@ func TestPermalinkExpansionSliceSyntax(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
exp, err := NewPermalinkExpander(urlize, nil)
|
||||
c.Assert(err, qt.IsNil)
|
||||
slice := []string{"a", "b", "c", "d"}
|
||||
fn := func(s string) []string {
|
||||
return exp.toSliceFunc(s)(slice)
|
||||
slice4 := []string{"a", "b", "c", "d"}
|
||||
fn4 := func(s string) []string {
|
||||
return exp.toSliceFunc(s)(slice4)
|
||||
}
|
||||
|
||||
slice1 := []string{"a"}
|
||||
fn1 := func(s string) []string {
|
||||
return exp.toSliceFunc(s)(slice1)
|
||||
}
|
||||
|
||||
c.Run("Basic", func(c *qt.C) {
|
||||
c.Assert(fn("[1:3]"), qt.DeepEquals, []string{"b", "c"})
|
||||
c.Assert(fn("[1:]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||
c.Assert(fn("[:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||
c.Assert(fn("[0:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||
c.Assert(fn("[:]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn(""), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn("[last]"), qt.DeepEquals, []string{"d"})
|
||||
c.Assert(fn("[:last]"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(fn4("[1:3]"), qt.DeepEquals, []string{"b", "c"})
|
||||
c.Assert(fn4("[1:]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||
c.Assert(fn4("[:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||
c.Assert(fn4("[0:2]"), qt.DeepEquals, []string{"a", "b"})
|
||||
c.Assert(fn4("[:]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn4(""), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn4("[last]"), qt.DeepEquals, []string{"d"})
|
||||
c.Assert(fn4("[:last]"), qt.DeepEquals, []string{"a", "b", "c"})
|
||||
c.Assert(fn1("[last]"), qt.DeepEquals, []string{"a"})
|
||||
c.Assert(fn1("[:last]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn1("[1:last]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn1("[1]"), qt.DeepEquals, []string{})
|
||||
|
||||
})
|
||||
|
||||
c.Run("Out of bounds", func(c *qt.C) {
|
||||
c.Assert(fn("[1:5]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||
c.Assert(fn("[-1:5]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn("[5:]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn("[5:]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn("[5:32]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn4("[1:5]"), qt.DeepEquals, []string{"b", "c", "d"})
|
||||
c.Assert(fn4("[-1:5]"), qt.DeepEquals, []string{"a", "b", "c", "d"})
|
||||
c.Assert(fn4("[5:]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn4("[5:]"), qt.DeepEquals, []string{})
|
||||
c.Assert(fn4("[5:32]"), qt.DeepEquals, []string{})
|
||||
c.Assert(exp.toSliceFunc("[:1]")(nil), qt.DeepEquals, []string(nil))
|
||||
c.Assert(exp.toSliceFunc("[:1]")([]string{}), qt.DeepEquals, []string(nil))
|
||||
|
||||
// These all return nil
|
||||
c.Assert(fn("[]"), qt.IsNil)
|
||||
c.Assert(fn("[1:}"), qt.IsNil)
|
||||
c.Assert(fn("foo"), qt.IsNil)
|
||||
c.Assert(fn4("[]"), qt.IsNil)
|
||||
c.Assert(fn4("[1:}"), qt.IsNil)
|
||||
c.Assert(fn4("foo"), qt.IsNil)
|
||||
|
||||
})
|
||||
|
||||
@@ -209,9 +228,12 @@ func BenchmarkPermalinkExpand(b *testing.B) {
|
||||
page.title = "Hugo Rocks"
|
||||
d, _ := time.Parse("2006-01-02", "2019-02-28")
|
||||
page.date = d
|
||||
page.kind = "page"
|
||||
|
||||
permalinksConfig := map[string]string{
|
||||
"posts": "/:year-:month-:title",
|
||||
permalinksConfig := map[string]map[string]string{
|
||||
"page": {
|
||||
"posts": "/:year-:month-:title",
|
||||
},
|
||||
}
|
||||
expander, err := NewPermalinkExpander(urlize, permalinksConfig)
|
||||
if err != nil {
|
||||
|
||||
@@ -181,7 +181,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
|
||||
if err != nil {
|
||||
if hexec.IsNotFound(err) {
|
||||
// This may be on a CI server etc. Will fall back to pre-built assets.
|
||||
return herrors.ErrFeatureNotAvailable
|
||||
return &herrors.FeatureNotAvailableError{Cause: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if hexec.IsNotFound(err) {
|
||||
return herrors.ErrFeatureNotAvailable
|
||||
return &herrors.FeatureNotAvailableError{Cause: err}
|
||||
}
|
||||
return fmt.Errorf(errBuf.String()+": %w", err)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
|
||||
opts.resolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
|
||||
opts.contents = string(src)
|
||||
opts.mediaType = ctx.InMediaType
|
||||
opts.tsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")
|
||||
|
||||
buildOptions, err := toBuildOptions(opts)
|
||||
if err != nil {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function hello3() {
|
||||
-- layouts/index.html --
|
||||
{{ $js := resources.Get "js/main.js" | js.Build }}
|
||||
JS Content:{{ $js.Content }}:End:
|
||||
|
||||
|
||||
`
|
||||
|
||||
c.Run("Basic", func(c *qt.C) {
|
||||
@@ -90,9 +90,9 @@ disableKinds=["page", "section", "taxonomy", "term", "sitemap", "robotsTXT"]
|
||||
path="github.com/gohugoio/hugoTestProjectJSModImports"
|
||||
-- go.mod --
|
||||
module github.com/gohugoio/tests/testHugoModules
|
||||
|
||||
|
||||
go 1.16
|
||||
|
||||
|
||||
require github.com/gohugoio/hugoTestProjectJSModImports v0.10.0 // indirect
|
||||
-- package.json --
|
||||
{
|
||||
@@ -100,7 +100,7 @@ require github.com/gohugoio/hugoTestProjectJSModImports v0.10.0 // indirect
|
||||
"date-fns": "^2.16.1"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
`
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
@@ -142,7 +142,7 @@ console.log("included");
|
||||
-- assets/js/main.js --
|
||||
import "./included";
|
||||
import { toCamelCase } from "to-camel-case";
|
||||
|
||||
|
||||
console.log("main");
|
||||
console.log("To camel:", toCamelCase("space case"));
|
||||
-- assets/js/myjsx.jsx --
|
||||
@@ -222,7 +222,7 @@ import { hello1, hello2 } from './util1';
|
||||
hello1();
|
||||
hello2();
|
||||
-- assets/js/util1.js --
|
||||
/* Some
|
||||
/* Some
|
||||
comments.
|
||||
*/
|
||||
import { hello3 } from './util2';
|
||||
@@ -239,7 +239,7 @@ export function hello3() {
|
||||
-- layouts/index.html --
|
||||
{{ $js := resources.Get "js/main.js" | js.Build }}
|
||||
JS Content:{{ $js.Content }}:End:
|
||||
|
||||
|
||||
`
|
||||
|
||||
c.Run("Import from main not found", func(c *qt.C) {
|
||||
@@ -292,9 +292,9 @@ import 'imp3/foo.js';
|
||||
}).Build()
|
||||
|
||||
expected := `
|
||||
IMPORT_SRC_DIR:imp1/index.js
|
||||
IMPORT_SRC_DIR:imp1/index.js
|
||||
IMPORT_SRC_DIR:imp2/index.ts
|
||||
IMPORT_SRC_DIR:imp3/foo.ts
|
||||
IMPORT_SRC_DIR:imp3/foo.ts
|
||||
`
|
||||
expected = strings.ReplaceAll(expected, "IMPORT_SRC_DIR", importSrcDir)
|
||||
|
||||
@@ -340,7 +340,36 @@ console.log("Hello 2");
|
||||
License util1
|
||||
License util2
|
||||
Main license
|
||||
|
||||
|
||||
`)
|
||||
|
||||
}
|
||||
|
||||
// Issue #11232
|
||||
func TestTypeScriptExperimentalDecorators(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['RSS','sitemap','taxonomy','term']
|
||||
-- tsconfig.json --
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
}
|
||||
}
|
||||
-- assets/ts/main.ts --
|
||||
function addFoo(target: any) {target.prototype.foo = 'bar'}
|
||||
@addFoo
|
||||
class A {}
|
||||
-- layouts/index.html --
|
||||
{{ $opts := dict "target" "es2020" "targetPath" "js/main.js" }}
|
||||
{{ (resources.Get "ts/main.ts" | js.Build $opts).Publish }}
|
||||
`
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
NeedsOsFS: true,
|
||||
TxtarString: files,
|
||||
}).Build()
|
||||
b.AssertFileContent("public/js/main.js", "__decorateClass")
|
||||
}
|
||||
|
||||
@@ -168,6 +168,25 @@ func TestTransformPostCSSError(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestTransformPostCSSNotInstalledError(t *testing.T) {
|
||||
if !htesting.IsCI() {
|
||||
t.Skip("Skip long running test when running locally")
|
||||
}
|
||||
|
||||
c := qt.New(t)
|
||||
|
||||
s, err := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: c,
|
||||
NeedsOsFS: true,
|
||||
TxtarString: postCSSIntegrationTestFiles,
|
||||
}).BuildE()
|
||||
|
||||
s.AssertIsFileError(err)
|
||||
c.Assert(err.Error(), qt.Contains, `binary with name "npx" not found`)
|
||||
|
||||
}
|
||||
|
||||
// #9895
|
||||
func TestTransformPostCSSImportError(t *testing.T) {
|
||||
if !htesting.IsCI() {
|
||||
|
||||
@@ -205,7 +205,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
|
||||
if err != nil {
|
||||
if hexec.IsNotFound(err) {
|
||||
// This may be on a CI server etc. Will fall back to pre-built assets.
|
||||
return herrors.ErrFeatureNotAvailable
|
||||
return &herrors.FeatureNotAvailableError{Cause: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -240,7 +240,9 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if hexec.IsNotFound(err) {
|
||||
return herrors.ErrFeatureNotAvailable
|
||||
return &herrors.FeatureNotAvailableError{
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
return imp.toFileError(errBuf.String())
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ func (r *resourceAdapter) transform(publish, setContent bool) error {
|
||||
newErr := func(err error) error {
|
||||
msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type)
|
||||
|
||||
if err == herrors.ErrFeatureNotAvailable {
|
||||
if herrors.IsFeatureNotAvailableError(err) {
|
||||
var errMsg string
|
||||
if tr.Key().Name == "postcss" {
|
||||
// This transformation is not available in this
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
+54
-20
@@ -1,16 +1,33 @@
|
||||
name: hugo
|
||||
issues: https://github.com/gohugoio/hugo/issues
|
||||
source-code: https://github.com/gohugoio/hugo.git
|
||||
website: https://gohugo.io/
|
||||
summary: Fast and Flexible Static Site Generator
|
||||
description: |
|
||||
Hugo is a static HTML and CSS website generator written in Go. It is
|
||||
optimized for speed, easy use and configurability. Hugo takes a directory
|
||||
with content and templates and renders them into a full HTML website.
|
||||
license: "Apache-2.0"
|
||||
base: core20
|
||||
confinement: strict
|
||||
adopt-info: hugo
|
||||
title: Hugo
|
||||
icon: snap/local/logo.png
|
||||
summary: Fast and Flexible Static Site Generator
|
||||
description: |
|
||||
Hugo is a static site generator written in Go, optimized for speed and
|
||||
designed for flexibility. With its advanced templating system and fast asset
|
||||
pipelines, Hugo renders a complete site in seconds, often less.
|
||||
|
||||
Due to its flexible framework, multilingual support, and powerful taxonomy
|
||||
system, Hugo is widely used to create:
|
||||
|
||||
* Corporate, government, nonprofit, education, news, event, and project sites
|
||||
* Documentation sites
|
||||
* Image portfolios
|
||||
* Landing pages
|
||||
* Business, professional, and personal blogs
|
||||
* Resumes and CVs
|
||||
|
||||
Use Hugo's embedded web server during development to instantly see changes to
|
||||
content, structure, behavior, and presentation. Then deploy the site to your
|
||||
host, or push changes to your Git provider for automated builds and
|
||||
deployment.
|
||||
issues: https://github.com/gohugoio/hugo/issues
|
||||
license: "Apache-2.0"
|
||||
source-code: https://github.com/gohugoio/hugo.git
|
||||
website: https://gohugo.io/
|
||||
|
||||
package-repositories:
|
||||
- type: apt
|
||||
@@ -28,8 +45,20 @@ plugs:
|
||||
interface: personal-files
|
||||
read:
|
||||
- $HOME/.gitconfig
|
||||
- $HOME/.config/git # Allows $HOME/.config/git/config and more
|
||||
- $HOME/.gitconfig.local # See #10337
|
||||
- $HOME/.config/git
|
||||
- $HOME/.gitconfig.local
|
||||
dot-aws:
|
||||
interface: personal-files
|
||||
read:
|
||||
- $HOME/.aws
|
||||
dot-azure:
|
||||
interface: personal-files
|
||||
read:
|
||||
- $HOME/.azure
|
||||
dot-config-gcloud:
|
||||
interface: personal-files
|
||||
read:
|
||||
- $HOME/.config/gcloud
|
||||
|
||||
environment:
|
||||
HOME: $SNAP_REAL_HOME
|
||||
@@ -41,6 +70,17 @@ environment:
|
||||
pandoc_datadir: $SNAP/usr/share/pandoc
|
||||
PYTHONHOME: /usr:$SNAP/usr
|
||||
RUBYLIB: $SNAP/usr/lib/ruby/vendor_ruby/2.7.0:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/ruby/vendor_ruby/2.7.0:$SNAP/usr/lib/ruby/vendor_ruby:$SNAP/usr/lib/ruby/2.7.0:$SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/ruby/2.7.0
|
||||
# HUGO_SECURITY_EXEC_OSENV
|
||||
#
|
||||
# Default value:
|
||||
# (?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$
|
||||
# Bundled applications require additional access:
|
||||
# git: GIT_EXEC_PATH and LD_LIBRARY_PATH
|
||||
# npx: npm_config_{cache,init_module,userconfig}
|
||||
# pandoc: pandoc_datadir
|
||||
# rst2html: PYTHONHOME and SNAP
|
||||
# asciidoctor: RUBYLIB
|
||||
HUGO_SECURITY_EXEC_OSENV: (?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|GIT_EXEC_PATH|LD_LIBRARY_PATH|npm_config_(cache|init_module|userconfig)|pandoc_datadir|PYTHONHOME|SNAP|RUBYLIB)$
|
||||
|
||||
apps:
|
||||
hugo:
|
||||
@@ -52,6 +92,9 @@ apps:
|
||||
- removable-media
|
||||
- etc-gitconfig
|
||||
- gitconfig
|
||||
- dot-aws
|
||||
- dot-azure
|
||||
- dot-config-gcloud
|
||||
|
||||
parts:
|
||||
git:
|
||||
@@ -92,15 +135,6 @@ parts:
|
||||
export GOPATH=$(realpath ../go)
|
||||
export PATH=$GOPATH/bin:$PATH
|
||||
|
||||
echo " * Patch securityConfig.go to allow"
|
||||
echo " - GIT_EXEC_PATH and LD_LIBRARY_PATH to be passed to git"
|
||||
echo " - npm_config_{cache,init_module,userconfig} to be passed to npx"
|
||||
echo " - pandoc_datadir to be passed to pandoc"
|
||||
echo " - PYTHONHOME and SNAP to be passed to rst2html"
|
||||
echo " - RUBYLIB to be passed to asciidoctor"
|
||||
sed -i '/OsEnv: NewWhitelist/s/)\$/|GIT_EXEC_PATH|LD_LIBRARY_PATH|npm_config_(cache|init_module|userconfig)|pandoc_datadir|PYTHONHOME|RUBYLIB|SNAP&/' config/security/securityConfig.go
|
||||
git diff config/security/securityConfig.go
|
||||
|
||||
HUGO_BUILD_TAGS="extended"
|
||||
echo " * Building hugo (HUGO_BUILD_TAGS=\"$HUGO_BUILD_TAGS\")..."
|
||||
go build -v -ldflags "-X github.com/gohugoio/hugo/common/hugo.vendorInfo=snap:$(git describe --tags --always --match 'v[0-9]*' | sed 's/^v//; s/-/+git/; s/-g/./')" -tags "$HUGO_BUILD_TAGS"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
hugo --printPathWarnings
|
||||
|
||||
! stdout 'Duplicate'
|
||||
|
||||
-- hugo.toml --
|
||||
-- assets/css/styles.css --
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
-- content/p1.md --
|
||||
-- content/p2.md --
|
||||
-- content/p3.md --
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
-- layouts/_default/single.html --
|
||||
{{ $css := resources.Get "css/styles.css" }}
|
||||
{{ $css := $css | minify | fingerprint | resources.PostProcess }}
|
||||
CSS: {{ $css.RelPermalink }}
|
||||
{{ .Title }}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
hugo --printPathWarnings
|
||||
|
||||
stdout 'Duplicate'
|
||||
|
||||
-- hugo.toml --
|
||||
-- assets/css/styles.css --
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
-- content/p1.md --
|
||||
---
|
||||
url: /p1/
|
||||
---
|
||||
-- content/p2.md --
|
||||
---
|
||||
url: /p1/
|
||||
---
|
||||
-- content/p3.md --
|
||||
---
|
||||
url: /p1/
|
||||
---
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
|
||||
@@ -22,6 +22,10 @@ stdout 'Deleted 2\d{2} files from module cache\.'
|
||||
cd submod
|
||||
hugo mod init testsubmod
|
||||
cmpenv go.mod $WORK/golden/go.mod.testsubmod
|
||||
hugo mod get -h
|
||||
stdout 'hugo mod get \[flags\] \[args\]'
|
||||
hugo mod get --help
|
||||
stdout 'hugo mod get \[flags\] \[args\]'
|
||||
-- hugo.toml --
|
||||
title = "Hugo Modules Test"
|
||||
[module]
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
dostounix golden/vendor.txt
|
||||
|
||||
hugo mod vendor
|
||||
cmp _vendor/modules.txt golden/vendor.txt
|
||||
ls _vendor/github.com/gohugoio/hugo-mod-integrationtests/withconfigtoml
|
||||
stdout 'config.toml'
|
||||
ls _vendor/github.com/gohugoio/hugo-mod-integrationtests/withhugotoml
|
||||
stdout 'hugo.toml'
|
||||
|
||||
|
||||
-- hugo.toml --
|
||||
title = "Hugo Modules Test"
|
||||
[module]
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugo-mod-integrationtests/withconfigtoml"
|
||||
[[module.imports]]
|
||||
path="github.com/gohugoio/hugo-mod-integrationtests/withhugotoml"
|
||||
-- go.mod --
|
||||
go 1.19
|
||||
|
||||
module github.com/gohugoio/testmod
|
||||
-- golden/vendor.txt --
|
||||
# github.com/gohugoio/hugo-mod-integrationtests/withconfigtoml v1.0.0
|
||||
# github.com/gohugoio/hugo-mod-integrationtests/withhugotoml v1.0.0
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
hugo new site -h
|
||||
stdout 'Create a new site in the provided directory'
|
||||
hugo new site my-yaml-site --format yml
|
||||
checkfile my-yaml-site/hugo.yml
|
||||
hugo new site mysite -f
|
||||
stdout 'Congratulations! Your new Hugo site is created in'
|
||||
cd mysite
|
||||
checkfile hugo.toml
|
||||
|
||||
hugo new theme -h
|
||||
stdout 'Create a new site in the provided directory'
|
||||
stdout 'Create a new theme \(skeleton\) called \[name\] in ./themes'
|
||||
hugo new theme mytheme
|
||||
stdout 'Creating theme'
|
||||
cd themes
|
||||
@@ -45,6 +47,3 @@ draft: true
|
||||
---
|
||||
|
||||
Dummy content.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math/rand"
|
||||
@@ -99,7 +100,7 @@ func (ns *Namespace) After(n any, l any) (any, error) {
|
||||
|
||||
// Delimit takes a given list l and returns a string delimited by sep.
|
||||
// If last is passed to the function, it will be used as the final delimiter.
|
||||
func (ns *Namespace) Delimit(l, sep any, last ...any) (template.HTML, error) {
|
||||
func (ns *Namespace) Delimit(ctx context.Context, l, sep any, last ...any) (template.HTML, error) {
|
||||
d, err := cast.ToStringE(sep)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -125,7 +126,7 @@ func (ns *Namespace) Delimit(l, sep any, last ...any) (template.HTML, error) {
|
||||
var str string
|
||||
switch lv.Kind() {
|
||||
case reflect.Map:
|
||||
sortSeq, err := ns.Sort(l)
|
||||
sortSeq, err := ns.Sort(ctx, l)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -166,9 +167,9 @@ func TestDelimit(t *testing.T) {
|
||||
var err error
|
||||
|
||||
if test.last == nil {
|
||||
result, err = ns.Delimit(test.seq, test.delimiter)
|
||||
result, err = ns.Delimit(context.Background(), test.seq, test.delimiter)
|
||||
} else {
|
||||
result, err = ns.Delimit(test.seq, test.delimiter, test.last)
|
||||
result, err = ns.Delimit(context.Background(), test.seq, test.delimiter, test.last)
|
||||
}
|
||||
|
||||
c.Assert(err, qt.IsNil, errMsg)
|
||||
|
||||
@@ -85,6 +85,57 @@ func TestAppendSliceToASliceOfSlices(t *testing.T) {
|
||||
{{ $obj = $obj | append (slice "b") }}
|
||||
{{ $obj = $obj | append (slice "c") }}
|
||||
|
||||
{{ $obj }}
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "[[a] [b] [c]]")
|
||||
|
||||
}
|
||||
|
||||
func TestAppendNilToSlice(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/index.html --
|
||||
{{ $obj := (slice "a") }}
|
||||
{{ $obj = $obj | append nil }}
|
||||
|
||||
{{ $obj }}
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "[a <nil>]")
|
||||
|
||||
}
|
||||
|
||||
func TestAppendNilsToSliceWithNils(t *testing.T) {
|
||||
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- layouts/index.html --
|
||||
{{ $obj := (slice "a" nil "c") }}
|
||||
{{ $obj = $obj | append nil }}
|
||||
|
||||
{{ $obj }}
|
||||
|
||||
|
||||
@@ -99,8 +150,49 @@ func TestAppendSliceToASliceOfSlices(t *testing.T) {
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", "[[a] [b] [c]]")
|
||||
b.AssertFileContent("public/index.html", "[a <nil> c <nil>]")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Issue 11234.
|
||||
func TestWhereWithWordCount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- config.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/index.html --
|
||||
Home: {{ range where site.RegularPages "WordCount" "gt" 50 }}{{ .Title }}|{{ end }}
|
||||
-- layouts/shortcodes/lorem.html --
|
||||
{{ "ipsum " | strings.Repeat (.Get 0 | int) }}
|
||||
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "p1"
|
||||
---
|
||||
{{< lorem 100 >}}
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "p2"
|
||||
---
|
||||
{{< lorem 20 >}}
|
||||
-- content/p3.md --
|
||||
---
|
||||
title: "p3"
|
||||
---
|
||||
{{< lorem 60 >}}
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
Home: p1|p3|
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
@@ -26,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
// Sort returns a sorted copy of the list l.
|
||||
func (ns *Namespace) Sort(l any, args ...any) (any, error) {
|
||||
func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error) {
|
||||
if l == nil {
|
||||
return nil, errors.New("sequence must be provided")
|
||||
}
|
||||
@@ -36,6 +37,8 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
|
||||
return nil, errors.New("can't iterate over a nil value")
|
||||
}
|
||||
|
||||
ctxv := reflect.ValueOf(ctx)
|
||||
|
||||
var sliceType reflect.Type
|
||||
switch seqv.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
@@ -78,7 +81,7 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
|
||||
v := p.Pairs[i].Value
|
||||
var err error
|
||||
for i, elemName := range path {
|
||||
v, err = evaluateSubElem(v, elemName)
|
||||
v, err = evaluateSubElem(ctxv, v, elemName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -108,7 +111,7 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
|
||||
v := p.Pairs[i].Value
|
||||
var err error
|
||||
for i, elemName := range path {
|
||||
v, err = evaluateSubElem(v, elemName)
|
||||
v, err = evaluateSubElem(ctxv, v, elemName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -240,9 +241,9 @@ func TestSort(t *testing.T) {
|
||||
var result any
|
||||
var err error
|
||||
if test.sortByField == nil {
|
||||
result, err = ns.Sort(test.seq)
|
||||
result, err = ns.Sort(context.Background(), test.seq)
|
||||
} else {
|
||||
result, err = ns.Sort(test.seq, test.sortByField, test.sortAsc)
|
||||
result, err = ns.Sort(context.Background(), test.seq, test.sortByField, test.sortAsc)
|
||||
}
|
||||
|
||||
if b, ok := test.expect.(bool); ok && !b {
|
||||
|
||||
+23
-12
@@ -14,6 +14,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -24,7 +25,7 @@ import (
|
||||
)
|
||||
|
||||
// Where returns a filtered subset of collection c.
|
||||
func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
|
||||
func (ns *Namespace) Where(ctx context.Context, c, key any, args ...any) (any, error) {
|
||||
seqv, isNil := indirect(reflect.ValueOf(c))
|
||||
if isNil {
|
||||
return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(c).Type().String())
|
||||
@@ -35,6 +36,8 @@ func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctxv := reflect.ValueOf(ctx)
|
||||
|
||||
var path []string
|
||||
kv := reflect.ValueOf(key)
|
||||
if kv.Kind() == reflect.String {
|
||||
@@ -43,9 +46,9 @@ func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
|
||||
|
||||
switch seqv.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
return ns.checkWhereArray(seqv, kv, mv, path, op)
|
||||
return ns.checkWhereArray(ctxv, seqv, kv, mv, path, op)
|
||||
case reflect.Map:
|
||||
return ns.checkWhereMap(seqv, kv, mv, path, op)
|
||||
return ns.checkWhereMap(ctxv, seqv, kv, mv, path, op)
|
||||
default:
|
||||
return nil, fmt.Errorf("can't iterate over %v", c)
|
||||
}
|
||||
@@ -275,7 +278,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) {
|
||||
func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, error) {
|
||||
if !obj.IsValid() {
|
||||
return zero, errors.New("can't evaluate an invalid value")
|
||||
}
|
||||
@@ -301,12 +304,20 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error)
|
||||
|
||||
index := hreflect.GetMethodIndexByName(objPtr.Type(), elemName)
|
||||
if index != -1 {
|
||||
var args []reflect.Value
|
||||
mt := objPtr.Type().Method(index)
|
||||
num := mt.Type.NumIn()
|
||||
maxNumIn := 1
|
||||
if num > 1 && mt.Type.In(1).Implements(hreflect.ContextInterface) {
|
||||
args = []reflect.Value{ctx}
|
||||
maxNumIn = 2
|
||||
}
|
||||
|
||||
switch {
|
||||
case mt.PkgPath != "":
|
||||
return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ)
|
||||
case mt.Type.NumIn() > 1:
|
||||
return zero, fmt.Errorf("%s is a method of type %s but requires more than 1 parameter", elemName, typ)
|
||||
case mt.Type.NumIn() > maxNumIn:
|
||||
return zero, fmt.Errorf("%s is a method of type %s but requires more than %d parameter", elemName, typ, maxNumIn)
|
||||
case mt.Type.NumOut() == 0:
|
||||
return zero, fmt.Errorf("%s is a method of type %s but returns no output", elemName, typ)
|
||||
case mt.Type.NumOut() > 2:
|
||||
@@ -316,7 +327,7 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error)
|
||||
case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
|
||||
return zero, fmt.Errorf("%s is a method of type %s returning two values but the second value is not an error type", elemName, typ)
|
||||
}
|
||||
res := objPtr.Method(mt.Index).Call([]reflect.Value{})
|
||||
res := objPtr.Method(mt.Index).Call(args)
|
||||
if len(res) == 2 && !res[1].IsNil() {
|
||||
return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error))
|
||||
}
|
||||
@@ -371,7 +382,7 @@ func parseWhereArgs(args ...any) (mv reflect.Value, op string, err error) {
|
||||
|
||||
// checkWhereArray handles the where-matching logic when the seqv value is an
|
||||
// Array or Slice.
|
||||
func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
|
||||
func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
|
||||
rv := reflect.MakeSlice(seqv.Type(), 0, 0)
|
||||
|
||||
for i := 0; i < seqv.Len(); i++ {
|
||||
@@ -385,7 +396,7 @@ func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string,
|
||||
vvv = rvv
|
||||
for i, elemName := range path {
|
||||
var err error
|
||||
vvv, err = evaluateSubElem(vvv, elemName)
|
||||
vvv, err = evaluateSubElem(ctxv, vvv, elemName)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -417,14 +428,14 @@ func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string,
|
||||
}
|
||||
|
||||
// checkWhereMap handles the where-matching logic when the seqv value is a Map.
|
||||
func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
|
||||
func (ns *Namespace) checkWhereMap(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
|
||||
rv := reflect.MakeMap(seqv.Type())
|
||||
keys := seqv.MapKeys()
|
||||
for _, k := range keys {
|
||||
elemv := seqv.MapIndex(k)
|
||||
switch elemv.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
r, err := ns.checkWhereArray(elemv, kv, mv, path, op)
|
||||
r, err := ns.checkWhereArray(ctxv, elemv, kv, mv, path, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -443,7 +454,7 @@ func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op
|
||||
|
||||
switch elemvv.Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
r, err := ns.checkWhereArray(elemvv, kv, mv, path, op)
|
||||
r, err := ns.checkWhereArray(ctxv, elemvv, kv, mv, path, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
@@ -641,9 +642,9 @@ func TestWhere(t *testing.T) {
|
||||
var err error
|
||||
|
||||
if len(test.op) > 0 {
|
||||
results, err = ns.Where(test.seq, test.key, test.op, test.match)
|
||||
results, err = ns.Where(context.Background(), test.seq, test.key, test.op, test.match)
|
||||
} else {
|
||||
results, err = ns.Where(test.seq, test.key, test.match)
|
||||
results, err = ns.Where(context.Background(), test.seq, test.key, test.match)
|
||||
}
|
||||
if b, ok := test.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
@@ -662,17 +663,17 @@ func TestWhere(t *testing.T) {
|
||||
}
|
||||
|
||||
var err error
|
||||
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1)
|
||||
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1)
|
||||
if err == nil {
|
||||
t.Errorf("Where called with none string op value didn't return an expected error")
|
||||
}
|
||||
|
||||
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2)
|
||||
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2)
|
||||
if err == nil {
|
||||
t.Errorf("Where called with more than two variable arguments didn't return an expected error")
|
||||
}
|
||||
|
||||
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a")
|
||||
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a")
|
||||
if err == nil {
|
||||
t.Errorf("Where called with no variable arguments didn't return an expected error")
|
||||
}
|
||||
@@ -842,7 +843,7 @@ func TestEvaluateSubElem(t *testing.T) {
|
||||
{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false},
|
||||
{reflect.ValueOf([]string{"foo", "bar"}), "1", false},
|
||||
} {
|
||||
result, err := evaluateSubElem(test.value, test.key)
|
||||
result, err := evaluateSubElem(reflect.ValueOf(context.Background()), test.value, test.key)
|
||||
if b, ok := test.expect.(bool); ok && !b {
|
||||
if err == nil {
|
||||
t.Errorf("[%d] evaluateSubElem didn't return an expected error", i)
|
||||
|
||||
+23
-7
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/bep/overlayfs"
|
||||
"github.com/gohugoio/hugo/resources/images"
|
||||
|
||||
// Importing image codecs for image.DecodeConfig
|
||||
@@ -31,23 +32,38 @@ import (
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// New returns a new instance of the images-namespaced template functions.
|
||||
func New(deps *deps.Deps) *Namespace {
|
||||
func New(d *deps.Deps) *Namespace {
|
||||
var readFileFs afero.Fs
|
||||
|
||||
// The docshelper script does not have or need all the dependencies set up.
|
||||
if d.PathSpec != nil {
|
||||
readFileFs = overlayfs.New(overlayfs.Options{
|
||||
Fss: []afero.Fs{
|
||||
d.PathSpec.BaseFs.Work,
|
||||
d.PathSpec.BaseFs.Content.Fs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &Namespace{
|
||||
Filters: &images.Filters{},
|
||||
cache: map[string]image.Config{},
|
||||
deps: deps,
|
||||
readFileFs: readFileFs,
|
||||
Filters: &images.Filters{},
|
||||
cache: map[string]image.Config{},
|
||||
deps: d,
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides template functions for the "images" namespace.
|
||||
type Namespace struct {
|
||||
*images.Filters
|
||||
cacheMu sync.RWMutex
|
||||
cache map[string]image.Config
|
||||
readFileFs afero.Fs
|
||||
cacheMu sync.RWMutex
|
||||
cache map[string]image.Config
|
||||
|
||||
deps *deps.Deps
|
||||
}
|
||||
@@ -73,7 +89,7 @@ func (ns *Namespace) Config(path any) (image.Config, error) {
|
||||
return config, nil
|
||||
}
|
||||
|
||||
f, err := ns.deps.Fs.WorkingDirReadOnly.Open(filename)
|
||||
f, err := ns.readFileFs.Open(filename)
|
||||
if err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import (
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/config/testconfig"
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/hugofs"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
@@ -86,11 +84,10 @@ func TestNSConfig(t *testing.T) {
|
||||
afs := afero.NewMemMapFs()
|
||||
v := config.New()
|
||||
v.Set("workingDir", "/a/b")
|
||||
conf := testconfig.GetTestConfig(afs, v)
|
||||
bcfg := conf.BaseConfig()
|
||||
fs := hugofs.NewFrom(afs, bcfg)
|
||||
d := testconfig.GetTestDeps(afs, v)
|
||||
bcfg := d.Conf
|
||||
|
||||
ns := New(&deps.Deps{Fs: fs, Conf: conf})
|
||||
ns := New(d)
|
||||
|
||||
for _, test := range configTests {
|
||||
|
||||
@@ -104,7 +101,7 @@ func TestNSConfig(t *testing.T) {
|
||||
// cast path to string for afero.WriteFile
|
||||
sp, err := cast.ToStringE(test.path)
|
||||
c.Assert(err, qt.IsNil)
|
||||
afero.WriteFile(ns.deps.Fs.Source, filepath.Join(bcfg.WorkingDir, sp), test.input, 0755)
|
||||
afero.WriteFile(ns.deps.Fs.Source, filepath.Join(bcfg.WorkingDir(), sp), test.input, 0755)
|
||||
|
||||
result, err := ns.Config(test.path)
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2023 The Hugo Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package images_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestImageConfigFromModule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
theme = ["mytheme"]
|
||||
-- static/images/pixel1.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- themes/mytheme/static/images/pixel2.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- layouts/index.html --
|
||||
{{ $path := "static/images/pixel1.png" }}
|
||||
fileExists OK: {{ fileExists $path }}|
|
||||
imageConfig OK: {{ (imageConfig $path).Width }}|
|
||||
{{ $path2 := "static/images/pixel2.png" }}
|
||||
fileExists2 OK: {{ fileExists $path2 }}|
|
||||
imageConfig2 OK: {{ (imageConfig $path2).Width }}|
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.NewIntegrationTestBuilder(
|
||||
hugolib.IntegrationTestConfig{
|
||||
T: t,
|
||||
TxtarString: files,
|
||||
},
|
||||
).Build()
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
fileExists OK: true|
|
||||
imageConfig OK: 1|
|
||||
fileExists2 OK: true|
|
||||
imageConfig2 OK: 1|
|
||||
`)
|
||||
}
|
||||
@@ -754,15 +754,15 @@ func (t *templateHandler) applyTemplateTransformers(ns *templateNamespace, ts *t
|
||||
//go:embed embedded/templates/*
|
||||
//go:embed embedded/templates/_default/*
|
||||
//go:embed embedded/templates/_server/*
|
||||
var embededTemplatesFs embed.FS
|
||||
var embeddedTemplatesFs embed.FS
|
||||
|
||||
func (t *templateHandler) loadEmbedded() error {
|
||||
return fs.WalkDir(embededTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if d == nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
templb, err := embededTemplatesFs.ReadFile(path)
|
||||
templb, err := embeddedTemplatesFs.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user