diff --git a/.circleci/config.yml b/.circleci/config.yml index 4702a8457..06e643bdd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ parameters: defaults: &defaults resource_class: large docker: - - image: bepsays/ci-hugoreleaser:1.22000.20100 + - image: bepsays/ci-hugoreleaser:1.22400.20000 environment: &buildenv GOMODCACHE: /root/project/gomodcache version: 2 @@ -14,9 +14,7 @@ jobs: environment: &buildenv GOMODCACHE: /root/project/gomodcache steps: - - &remote-docker - setup_remote_docker: - version: 20.10.14 + - setup_remote_docker - checkout: path: hugo - &git-config @@ -60,7 +58,7 @@ jobs: environment: <<: [*buildenv] docker: - - image: bepsays/ci-hugoreleaser-linux-arm64:1.22000.20100 + - image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000 steps: - *restore-cache - &attach-workspace diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 250b67a9b..fa2791492 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,7 +5,12 @@ assignees: '' about: Create a report to help us improve --- - + + ### What version of Hugo are you using (`hugo version`)? diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 000000000..c4f3c34c3 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,49 @@ +name: Build Docker image + +on: + release: + types: [published] + pull_request: +permissions: + packages: write + +env: + REGISTRY_IMAGE: ghcr.io/gohugoio/hugo + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Docker meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ env.REGISTRY_IMAGE }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + + - name: Login to GHCR + # Login is only needed when the image is pushed + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + id: build + uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1 + with: + context: . + provenance: mode=max + sbom: true + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: HUGO_BUILD_TAGS=extended,withdeploy \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 53b8adac4..249c1ab54 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@08e671be8ac8944d0e132aa71d0ae8ccfb347675 + - uses: dessant/lock-threads@7de207be1d3ce97a9abe6ff1306222982d1ca9f9 # v5.0.1 with: issue-inactive-days: 21 add-issue-labels: 'Outdated' @@ -24,7 +24,7 @@ jobs: This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. - - uses: actions/stale@04a1828bc18ada028d85a0252a47cd2963a91abe + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: operations-per-run: 999 days-before-issue-stale: 365 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26156ec94..c49c12371 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,94 +1,132 @@ on: push: - branches: [ master ] + branches: [master] pull_request: name: Test env: - GOPROXY: https://proxy.golang.org - GO111MODULE: on - DART_SASS_VERSION: 1.56.2 - DART_SASS_SHA_LINUX: 9e4f455f7b8619959d7878af2862383be58392eb963a14ff87cc512c03701e2a - DART_SASS_SHA_MACOS: 5992e979e2c30ec363f8e338822bb2b4443c74232b3340501a76180f5652cb09 - DART_SASS_SHA_WINDOWS: 8d3d9117c54840e3e6a4919e43acf75ea52f28a64fc87a8e29d80ec72ee36cfb + GOPROXY: https://proxy.golang.org + GO111MODULE: on + SASS_VERSION: 1.80.3 + DART_SASS_SHA_LINUX: 7c933edbad0a7d389192c5b79393485c088bd2c4398e32f5754c32af006a9ffd + DART_SASS_SHA_MACOS: 79e060b0e131c3bb3c16926bafc371dc33feab122bfa8c01aa337a072097967b + DART_SASS_SHA_WINDOWS: 0bc4708b37cd1bac4740e83ac5e3176e66b774f77fd5dd364da5b5cfc9bfb469 permissions: contents: read jobs: test: strategy: matrix: - go-version: [1.19.x,1.20.x] - os: [ubuntu-latest, macos-latest, windows-latest] + go-version: [1.23.x, 1.24.x] + os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues. runs-on: ${{ matrix.os }} steps: - - name: Checkout code - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - - name: Install Go - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f - with: - go-version: ${{ matrix.go-version }} - check-latest: true - cache: true - cache-dependency-path: | - **/go.sum - **/go.mod - - name: Install Ruby - uses: ruby/setup-ruby@ee2113536afb7f793eed4ce60e8d3b26db912da4 - with: - ruby-version: '2.7' - bundler-cache: true # - - name: Install Python - uses: actions/setup-python@3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df - with: - python-version: '3.x' - - name: Install Mage - run: go install github.com/magefile/mage@07afc7d24f4d6d6442305d49552f04fbda5ccb3e - - name: Install asciidoctor - uses: reitzig/actions-asciidoctor@7570212ae20b63653481675fb1ff62d1073632b0 - - name: Install docutils - run: | - pip install docutils - rst2html.py --version - - if: matrix.os == 'ubuntu-latest' - name: Install pandoc on Linux - run: | + - if: matrix.os == 'ubuntu-latest' + name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Install Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + cache: true + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Install Ruby + uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 + with: + ruby-version: "2.7" + bundler-cache: true # + - name: Install Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 + with: + python-version: "3.x" + - name: Install Mage + run: go install github.com/magefile/mage@v1.15.0 + - name: Install asciidoctor + uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2 + - name: Install docutils + run: | + pip install docutils + rst2html --version + - if: matrix.os == 'ubuntu-latest' + name: Install pandoc on Linux + run: | sudo apt-get update -y sudo apt-get install -y pandoc - - if: matrix.os == 'macos-latest' - run: | - brew install pandoc - - if: matrix.os == 'windows-latest' - run: | - Choco-Install -PackageName pandoc - - run: pandoc -v - - if: matrix.os == 'windows-latest' - run: | - Choco-Install -PackageName mingw -ArgumentList "--version","10.2.0","--allow-downgrade" - - if: matrix.os == 'ubuntu-latest' - name: Install dart-sass-embedded Linux - run: | - echo "Install Dart Sass version ${DART_SASS_VERSION} ..." - curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz"; - echo "${DART_SASS_SHA_LINUX} sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c; - tar -xvf "sass_embedded-${DART_SASS_VERSION}-linux-x64.tar.gz"; - echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH - - if: matrix.os == 'macos-latest' - name: Install dart-sass-embedded MacOS - run: | - echo "Install Dart Sass version ${DART_SASS_VERSION} ..." - curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${DART_SASS_VERSION}/sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz"; - echo "${DART_SASS_SHA_MACOS} sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c; - tar -xvf "sass_embedded-${DART_SASS_VERSION}-macos-x64.tar.gz"; - echo "$GITHUB_WORKSPACE/sass_embedded/" >> $GITHUB_PATH - - if: matrix.os == 'windows-latest' - name: Install dart-sass-embedded Windows - run: | - echo "Install Dart Sass version ${env:DART_SASS_VERSION} ..." - curl -LJO "https://github.com/sass/dart-sass-embedded/releases/download/${env:DART_SASS_VERSION}/sass_embedded-${env:DART_SASS_VERSION}-windows-x64.zip"; - Expand-Archive -Path "sass_embedded-${env:DART_SASS_VERSION}-windows-x64.zip" -DestinationPath .; - echo "$env:GITHUB_WORKSPACE/sass_embedded/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append - - name: Check - run: | - mage -v check; - env: - HUGO_BUILD_TAGS: extended - + - if: matrix.os == 'macos-latest' + run: | + brew install pandoc + - if: matrix.os == 'windows-latest' + run: | + choco install pandoc + - run: pandoc -v + - if: matrix.os == 'windows-latest' + run: | + choco install mingw + - if: matrix.os == 'ubuntu-latest' + name: Install dart-sass Linux + run: | + echo "Install Dart Sass version ${SASS_VERSION} ..." + curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-linux-x64.tar.gz"; + echo "${DART_SASS_SHA_LINUX} dart-sass-${SASS_VERSION}-linux-x64.tar.gz" | sha256sum -c; + tar -xvf "dart-sass-${SASS_VERSION}-linux-x64.tar.gz"; + echo "$GOBIN" + echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH + - if: matrix.os == 'macos-latest' + name: Install dart-sass MacOS + run: | + echo "Install Dart Sass version ${SASS_VERSION} ..." + curl -LJO "https://github.com/sass/dart-sass/releases/download/${SASS_VERSION}/dart-sass-${SASS_VERSION}-macos-x64.tar.gz"; + echo "${DART_SASS_SHA_MACOS} dart-sass-${SASS_VERSION}-macos-x64.tar.gz" | shasum -a 256 -c; + tar -xvf "dart-sass-${SASS_VERSION}-macos-x64.tar.gz"; + echo "$GITHUB_WORKSPACE/dart-sass/" >> $GITHUB_PATH + - if: matrix.os == 'windows-latest' + name: Install dart-sass Windows + run: | + echo "Install Dart Sass version ${env:SASS_VERSION} ..." + curl -LJO "https://github.com/sass/dart-sass/releases/download/${env:SASS_VERSION}/dart-sass-${env:SASS_VERSION}-windows-x64.zip"; + Expand-Archive -Path "dart-sass-${env:SASS_VERSION}-windows-x64.zip" -DestinationPath .; + echo "$env:GITHUB_WORKSPACE/dart-sass/" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf-8 -Append + - if: matrix.os == 'ubuntu-latest' + name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + - if: matrix.os == 'ubuntu-latest' + name: Run staticcheck + run: staticcheck ./... + - if: matrix.os != 'windows-latest' + name: Check + run: | + sass --version; + mage -v check; + env: + HUGO_BUILD_TAGS: extended,withdeploy + - if: matrix.os == 'windows-latest' + # See issue #11052. We limit the build to regular test (no -race flag) on Windows for now. + name: Test + run: | + mage -v test; + env: + HUGO_BUILD_TAGS: extended,withdeploy + - name: Build tags + run: | + go install -tags extended + - if: matrix.os == 'ubuntu-latest' + name: Build for dragonfly + run: | + go install + env: + GOARCH: amd64 + GOOS: dragonfly diff --git a/.gitignore b/.gitignore index b170fe204..ddad69611 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *.test -imports.* \ No newline at end of file +imports.* +dist/ +public/ +.DS_Store \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06e8a44d9..ddd3efcf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ ->**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future. +>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future. # Contributing to Hugo @@ -93,6 +93,7 @@ Most title/subjects should have a lower-cased prefix with a colon and one whites * If this commit touches many packages without a common functional topic, prefix with `all:` (e.g. `all: Reformat Go code`) * If this is a documentation update, prefix with `docs:`. * If nothing of the above applies, just leave the prefix out. +* Note that the above excludes nouns seen in other repositories, e.g. "chore:". Also, if your commit references one or more GitHub issues, always end your commit message body with *See #1234* or *Fixes #1234*. Replace *1234* with the GitHub issue ID. The last example will close the issue when the commit is merged into *master*. @@ -122,8 +123,6 @@ cd hugo go install ``` ->Note: Some Go tools may not be fully updated to support Go Modules yet. One example would be LiteIDE. Follow [this workaround](https://github.com/visualfc/liteide/issues/986#issuecomment-428117702) for how to continue to work with Hugo below `GOPATH`. - For some convenient build and test targets, you also will want to install Mage: ```bash diff --git a/Dockerfile b/Dockerfile index 7d0980035..a0e34353f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -2,44 +2,98 @@ # Twitter: https://twitter.com/gohugoio # Website: https://gohugo.io/ -FROM golang:1.19-alpine AS build +ARG GO_VERSION="1.24" +ARG ALPINE_VERSION="3.22" +ARG DART_SASS_VERSION="1.79.3" -# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so: -# docker build --build-arg HUGO_BUILD_TAGS=extended . -ARG HUGO_BUILD_TAGS +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gobuild +FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS gorun -ARG CGO=1 -ENV CGO_ENABLED=${CGO} -ENV GOOS=linux -ENV GO111MODULE=on + +FROM gobuild AS build + +RUN apk add clang lld + +# Set up cross-compilation helpers +COPY --from=xx / / + +ARG TARGETPLATFORM +RUN xx-apk add musl-dev gcc g++ + +# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so: +# docker build --build-arg HUGO_BUILD_TAGS=withdeploy . +# +# We build the extended version by default. +ARG HUGO_BUILD_TAGS="extended" +ENV CGO_ENABLED=1 +ENV GOPROXY=https://proxy.golang.org +ENV GOCACHE=/root/.cache/go-build +ENV GOMODCACHE=/go/pkg/mod +ARG TARGETPLATFORM WORKDIR /go/src/github.com/gohugoio/hugo -COPY . /go/src/github.com/gohugoio/hugo/ +# For --mount=type=cache the value of target is the default cache id, so +# for the go mod cache it would be good if we could share it with other Go images using the same setup, +# but the go build cache needs to be per platform. +# See this comment: https://github.com/moby/buildkit/issues/1706#issuecomment-702238282 +RUN --mount=target=. \ + --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build,id=go-build-$TARGETPLATFORM <Hugo -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) +--- [![GoDoc](https://godoc.org/github.com/gohugoio/hugo?status.svg)](https://godoc.org/github.com/gohugoio/hugo) [![Tests on Linux, MacOS and Windows](https://github.com/gohugoio/hugo/workflows/Test/badge.svg)](https://github.com/gohugoio/hugo/actions?query=workflow%3ATest) [![Go Report Card](https://goreportcard.com/badge/github.com/gohugoio/hugo)](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] | Mastodon ## 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: + +- Image processing – Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data +- JavaScript bundling – Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing. +- Sass processing – Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS +- Tailwind CSS processing – Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS + +And with [Hugo Modules], you can share content, assets, data, translations, themes, templates, and configuration with other projects via public or private Git repositories. + +See the [features] section of the documentation for a comprehensive summary of Hugo's capabilities. + +## Sponsors -## Banner Sponsors

 

- Linode           -

 

+ Linode +    + The complete IDE crafted for professional Go developers. +     + PinMe. +

-## Supported Architectures +## Editions -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. +Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features. -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. +Feature|extended edition|extended/deploy edition +:--|:-:|:-: +Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark: +[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark: +Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See [details].|:x:|:heavy_check_mark: -**Complete documentation is available at [Hugo Documentation](https://gohugo.io/getting-started/).** +[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass +[processing images]: https://gohugo.io/content-management/image-processing/ +[transpile sass to css]: https://gohugo.io/functions/css/sass/ +[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/ -## Choose How to Install +Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition. -If you want to use Hugo as your site generator, simply install the Hugo binaries. +## Installation -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. +Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system: -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. +- [macOS] +- [Linux] +- [Windows] +- [DragonFly BSD, FreeBSD, NetBSD, and OpenBSD] -### Install Hugo as Your Site Generator (Binary Install) +## Build from source -Use the [installation instructions in the Hugo documentation](https://gohugo.io/getting-started/installing/). +Prerequisites to build Hugo from source: -### Build and Install the Binary from Source (Using the Go toolchain) +- Standard edition: Go 1.23.0 or later +- Extended edition: Go 1.23.0 or later, and GCC +- Extended/deploy edition: Go 1.23.0 or later, and GCC -#### Prerequisite Tools +Build the standard edition: -* [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 +Build the extended/deploy edition: -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: - -```bash -git clone git@github.com:gohugoio/hugo.git +```text +CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest ``` -## Contributing code to Hugo + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=gohugoio/hugo&type=Timeline)](https://star-history.com/#gohugoio/hugo&Timeline) + +## Documentation + +Hugo's [documentation] includes installation instructions, a quick start guide, conceptual explanations, reference information, and examples. + +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. +
+See current dependencies -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 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/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/PuerkitoBio/goquery="v1.10.1" +github.com/alecthomas/chroma/v2="v2.15.0" +github.com/andybalholm/cascadia="v1.3.3" +github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c" +github.com/bep/clocks="v0.5.0" github.com/bep/debounce="v1.2.0" -github.com/bep/gitmap="v1.1.2" +github.com/bep/gitmap="v1.6.0" github.com/bep/goat="v0.5.0" -github.com/bep/godartsass="v0.16.0" -github.com/bep/golibsass="v1.1.0" -github.com/bep/gowebp="v0.2.0" -github.com/bep/lazycache="v0.2.0" -github.com/bep/overlayfs="v0.6.0" +github.com/bep/godartsass/v2="v2.3.2" +github.com/bep/golibsass="v1.2.0" +github.com/bep/gowebp="v0.3.0" +github.com/bep/imagemeta="v0.8.4" +github.com/bep/lazycache="v0.7.0" +github.com/bep/logg="v0.4.0" +github.com/bep/mclib="v1.20400.20402" +github.com/bep/overlayfs="v0.9.2" +github.com/bep/simplecobra="v0.5.0" github.com/bep/tmc="v0.5.1" -github.com/clbanning/mxj/v2="v2.5.7" -github.com/cli/safeexec="v1.0.0" -github.com/cpuguy83/go-md2man/v2="v2.0.2" +github.com/cespare/xxhash/v2="v2.3.0" +github.com/clbanning/mxj/v2="v2.7.0" +github.com/cpuguy83/go-md2man/v2="v2.0.4" 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/fsnotify/fsnotify="v1.6.0" -github.com/getkin/kin-openapi="v0.110.0" +github.com/dlclark/regexp2="v1.11.5" +github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17" +github.com/evanw/esbuild="v0.24.2" +github.com/fatih/color="v1.18.0" +github.com/frankban/quicktest="v1.14.6" +github.com/fsnotify/fsnotify="v1.8.0" +github.com/getkin/kin-openapi="v0.129.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.21.0" +github.com/go-openapi/swag="v0.23.0" +github.com/go-sourcemap/sourcemap="v2.1.4+incompatible" +github.com/gobuffalo/flect="v1.0.3" github.com/gobwas/glob="v0.2.3" -github.com/gohugoio/go-i18n/v2="v2.1.3-0.20210430103248-4c28c89f8013" +github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e" +github.com/gohugoio/hashstructure="v0.5.0" +github.com/gohugoio/httpcache="v0.7.0" +github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0" +github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0" 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/groupcache="v0.0.0-20210331224755-41bb18bfe9da" -github.com/golang/protobuf="v1.5.2" -github.com/google/go-cmp="v0.5.9" -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/gorilla/websocket="v1.5.0" -github.com/hairyhenderson/go-codeowners="v0.2.3-0.20201026200250-cdc7c0759690" -github.com/hashicorp/golang-lru/v2="v2.0.1" -github.com/invopop/yaml="v0.1.0" +github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0" +github.com/google/go-cmp="v0.6.0" +github.com/google/pprof="v0.0.0-20250208200701-d0013a598941" +github.com/gorilla/websocket="v1.5.3" +github.com/hairyhenderson/go-codeowners="v0.7.0" +github.com/hashicorp/golang-lru/v2="v2.0.7" 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/marekm4/color-extractor="v1.2.0" -github.com/mattn/go-ieproxy="v0.0.1" -github.com/mattn/go-isatty="v0.0.17" +github.com/kyokomi/emoji/v2="v2.2.13" +github.com/lucasb-eyer/go-colorful="v1.2.0" +github.com/mailru/easyjson="v0.7.7" +github.com/makeworld-the-better-one/dither/v2="v2.4.0" +github.com/marekm4/color-extractor="v1.2.1" +github.com/mattn/go-colorable="v0.1.13" +github.com/mattn/go-isatty="v0.0.20" github.com/mattn/go-runewidth="v0.0.9" -github.com/mitchellh/hashstructure="v1.1.0" -github.com/mitchellh/mapstructure="v1.5.0" +github.com/mazznoer/csscolorparser="v0.1.5" +github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c" 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/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349" +github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80" 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/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58" +github.com/pelletier/go-toml/v2="v2.2.3" +github.com/perimeterx/marshmallow="v1.1.5" +github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c" +github.com/pkg/errors="v0.9.1" +github.com/rivo/uniseg="v0.4.7" +github.com/rogpeppe/go-internal="v1.13.1" 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/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/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/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" -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" +github.com/sass/libsass="3.6.6" +github.com/spf13/afero="v1.11.0" +github.com/spf13/cast="v1.7.1" +github.com/spf13/cobra="v1.8.1" +github.com/spf13/fsync="v0.10.1" +github.com/spf13/pflag="v1.0.6" +github.com/tdewolff/minify/v2="v2.20.37" +github.com/tdewolff/parse/v2="v2.7.15" +github.com/tetratelabs/wazero="v1.8.2" +github.com/webmproject/libwebp="v1.3.2" +github.com/yuin/goldmark-emoji="v1.0.4" +github.com/yuin/goldmark="v1.7.8" +go.uber.org/automaxprocs="v1.5.3" +golang.org/x/crypto="v0.33.0" +golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac" +golang.org/x/image="v0.24.0" +golang.org/x/mod="v0.23.0" +golang.org/x/net="v0.35.0" +golang.org/x/sync="v0.11.0" +golang.org/x/sys="v0.30.0" +golang.org/x/text="v0.22.0" +golang.org/x/tools="v0.30.0" +golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da" +gonum.org/v1/plot="v0.15.0" +google.golang.org/protobuf="v1.36.5" gopkg.in/yaml.v2="v2.4.0" gopkg.in/yaml.v3="v3.0.1" +oss.terrastruct.com/d2="v0.6.9" +oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896" +rsc.io/qr="v0.2.0" +software.sslmate.com/src/go-pkcs12="v0.2.0" ``` +
diff --git a/SECURITY.md b/SECURITY.md index 320b2ff54..6ac90f072 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,4 @@ Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days. -Also see [Hugo's Security Model](https://gohugo.io/about/security-model/). +Also see [Hugo's Security Model](https://gohugo.io/about/security/). diff --git a/bench.sh b/bench.sh deleted file mode 100755 index c6a20a7e3..000000000 --- a/bench.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# allow user to override go executable by running as GOEXE=xxx make ... -GOEXE="${GOEXE-go}" - -# Convenience script to -# - For a given branch -# - Run benchmark tests for a given package -# - Do the same for master -# - then compare the two runs with benchcmp - -benchFilter=".*" - -if (( $# < 2 )); - then - echo "USAGE: ./bench.sh (and (regexp, optional))" - exit 1 -fi - - - -if [ $# -eq 3 ]; then - benchFilter=$3 -fi - - -BRANCH=$1 -PACKAGE=$2 - -git checkout $BRANCH -"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-$BRANCH.txt - -git checkout master -"${GOEXE}" test -test.run=NONE -bench="$benchFilter" -test.benchmem=true ./$PACKAGE > /tmp/bench-$PACKAGE-master.txt - - -benchcmp /tmp/bench-$PACKAGE-master.txt /tmp/bench-$PACKAGE-$BRANCH.txt diff --git a/benchSite.sh b/benchSite.sh deleted file mode 100755 index aae21231c..000000000 --- a/benchSite.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# allow user to override go executable by running as GOEXE=xxx make ... -GOEXE="${GOEXE-go}" - -# Send in a regexp matching the benchmarks you want to run, i.e. './benchSite.sh "YAML"'. -# Note the quotes, which will be needed for more complex expressions. -# The above will run all variations, but only for front matter YAML. - -echo "Running with BenchmarkSiteBuilding/${1}" - -"${GOEXE}" test -run="NONE" -bench="BenchmarkSiteBuilding/${1}" -test.benchmem=true ./hugolib -memprofile mem.prof -count 3 -cpuprofile cpu.prof diff --git a/benchbep.sh b/benchbep.sh deleted file mode 100755 index efd616c88..000000000 --- a/benchbep.sh +++ /dev/null @@ -1 +0,0 @@ -gobench -package=./hugolib -bench="BenchmarkSiteNew/Deep_content_tree" \ No newline at end of file diff --git a/bepdock.sh b/bepdock.sh deleted file mode 100755 index a7ac0c639..000000000 --- a/bepdock.sh +++ /dev/null @@ -1 +0,0 @@ -docker run --rm --mount type=bind,source="$(pwd)",target=/hugo -w /hugo -i -t bepsays/ci-goreleaser:1.11-2 /bin/bash \ No newline at end of file diff --git a/cache/docs.go b/cache/docs.go index babecec22..b9c49840f 100644 --- a/cache/docs.go +++ b/cache/docs.go @@ -1,2 +1,2 @@ -// Package cache contains the differenct cache implementations. +// Package cache contains the different cache implementations. package cache diff --git a/cache/dynacache/dynacache.go b/cache/dynacache/dynacache.go new file mode 100644 index 000000000..25d0f9b29 --- /dev/null +++ b/cache/dynacache/dynacache.go @@ -0,0 +1,647 @@ +// Copyright 2024 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 dynacache + +import ( + "context" + "fmt" + "math" + "path" + "regexp" + "runtime" + "sync" + "time" + + "github.com/bep/lazycache" + "github.com/bep/logg" + "github.com/gohugoio/hugo/common/collections" + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/rungroup" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/resources/resource" +) + +const minMaxSize = 10 + +type KeyIdentity struct { + Key any + Identity identity.Identity +} + +// New creates a new cache. +func New(opts Options) *Cache { + if opts.CheckInterval == 0 { + opts.CheckInterval = time.Second * 2 + } + + if opts.MaxSize == 0 { + opts.MaxSize = 100000 + } + if opts.Log == nil { + panic("nil Log") + } + + if opts.MinMaxSize == 0 { + opts.MinMaxSize = 30 + } + + stats := &stats{ + opts: opts, + adjustmentFactor: 1.0, + currentMaxSize: opts.MaxSize, + availableMemory: config.GetMemoryLimit(), + } + + infol := opts.Log.InfoCommand("dynacache") + + evictedIdentities := collections.NewStack[KeyIdentity]() + + onEvict := func(k, v any) { + if !opts.Watching { + return + } + identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool { + evictedIdentities.Push(KeyIdentity{Key: k, Identity: id}) + return false + }) + resource.MarkStale(v) + } + + c := &Cache{ + partitions: make(map[string]PartitionManager), + onEvict: onEvict, + evictedIdentities: evictedIdentities, + opts: opts, + stats: stats, + infol: infol, + } + + c.stop = c.start() + + return c +} + +// Options for the cache. +type Options struct { + Log loggers.Logger + CheckInterval time.Duration + MaxSize int + MinMaxSize int + Watching bool +} + +// Options for a partition. +type OptionsPartition struct { + // When to clear the this partition. + ClearWhen ClearWhen + + // Weight is a number between 1 and 100 that indicates how, in general, how big this partition may get. + Weight int +} + +func (o OptionsPartition) WeightFraction() float64 { + return float64(o.Weight) / 100 +} + +func (o OptionsPartition) CalculateMaxSize(maxSizePerPartition int) int { + return int(math.Floor(float64(maxSizePerPartition) * o.WeightFraction())) +} + +// A dynamic partitioned cache. +type Cache struct { + mu sync.RWMutex + + partitions map[string]PartitionManager + + onEvict func(k, v any) + evictedIdentities *collections.Stack[KeyIdentity] + + opts Options + infol logg.LevelLogger + + stats *stats + stopOnce sync.Once + stop func() +} + +// DrainEvictedIdentities drains the evicted identities from the cache. +func (c *Cache) DrainEvictedIdentities() []KeyIdentity { + return c.evictedIdentities.Drain() +} + +// DrainEvictedIdentitiesMatching drains the evicted identities from the cache that match the given predicate. +func (c *Cache) DrainEvictedIdentitiesMatching(predicate func(KeyIdentity) bool) []KeyIdentity { + return c.evictedIdentities.DrainMatching(predicate) +} + +// ClearMatching clears all partition for which the predicate returns true. +func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) { + if predicatePartition == nil { + predicatePartition = func(k string, p PartitionManager) bool { return true } + } + if predicateValue == nil { + panic("nil predicateValue") + } + g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{ + NumWorkers: len(c.partitions), + Handle: func(ctx context.Context, partition PartitionManager) error { + partition.clearMatching(predicateValue) + return nil + }, + }) + + for k, p := range c.partitions { + if !predicatePartition(k, p) { + continue + } + g.Enqueue(p) + } + + g.Wait() +} + +// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account. +// predicate is optional and will clear any entry for which it returns true. +func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) { + g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{ + NumWorkers: len(c.partitions), + Handle: func(ctx context.Context, partition PartitionManager) error { + partition.clearOnRebuild(predicate, changeset...) + return nil + }, + }) + + for _, p := range c.partitions { + g.Enqueue(p) + } + + g.Wait() + + // Clear any entries marked as stale above. + g = rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{ + NumWorkers: len(c.partitions), + Handle: func(ctx context.Context, partition PartitionManager) error { + partition.clearStale() + return nil + }, + }) + + for _, p := range c.partitions { + g.Enqueue(p) + } + + g.Wait() +} + +type keysProvider interface { + Keys() []string +} + +// Keys returns a list of keys in all partitions. +func (c *Cache) Keys(predicate func(s string) bool) []string { + if predicate == nil { + predicate = func(s string) bool { return true } + } + var keys []string + for pn, g := range c.partitions { + pkeys := g.(keysProvider).Keys() + for _, k := range pkeys { + p := path.Join(pn, k) + if predicate(p) { + keys = append(keys, p) + } + } + + } + return keys +} + +func calculateMaxSizePerPartition(maxItemsTotal, totalWeightQuantity, numPartitions int) int { + if numPartitions == 0 { + panic("numPartitions must be > 0") + } + if totalWeightQuantity == 0 { + panic("totalWeightQuantity must be > 0") + } + + avgWeight := float64(totalWeightQuantity) / float64(numPartitions) + return int(math.Floor(float64(maxItemsTotal) / float64(numPartitions) * (100.0 / avgWeight))) +} + +// Stop stops the cache. +func (c *Cache) Stop() { + c.stopOnce.Do(func() { + c.stop() + }) +} + +func (c *Cache) adjustCurrentMaxSize() { + c.mu.RLock() + defer c.mu.RUnlock() + + if len(c.partitions) == 0 { + return + } + var m runtime.MemStats + runtime.ReadMemStats(&m) + s := c.stats + s.memstatsCurrent = m + // fmt.Printf("\n\nAvailable = %v\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\nMaxSize = %d\nAdjustmentFactor=%f\n\n", helpers.FormatByteCount(s.availableMemory), helpers.FormatByteCount(m.Alloc), helpers.FormatByteCount(m.TotalAlloc), helpers.FormatByteCount(m.Sys), m.NumGC, c.stats.currentMaxSize, s.adjustmentFactor) + + if s.availableMemory >= s.memstatsCurrent.Alloc { + if s.adjustmentFactor <= 1.0 { + s.adjustmentFactor += 0.2 + } + } else { + // We're low on memory. + s.adjustmentFactor -= 0.4 + } + + if s.adjustmentFactor <= 0 { + s.adjustmentFactor = 0.05 + } + + if !s.adjustCurrentMaxSize() { + return + } + + totalWeight := 0 + for _, pm := range c.partitions { + totalWeight += pm.getOptions().Weight + } + + maxSizePerPartition := calculateMaxSizePerPartition(c.stats.currentMaxSize, totalWeight, len(c.partitions)) + + evicted := 0 + for _, p := range c.partitions { + evicted += p.adjustMaxSize(p.getOptions().CalculateMaxSize(maxSizePerPartition)) + } + + if evicted > 0 { + c.infol. + WithFields( + logg.Fields{ + {Name: "evicted", Value: evicted}, + {Name: "numGC", Value: m.NumGC}, + {Name: "limit", Value: helpers.FormatByteCount(c.stats.availableMemory)}, + {Name: "alloc", Value: helpers.FormatByteCount(m.Alloc)}, + {Name: "totalAlloc", Value: helpers.FormatByteCount(m.TotalAlloc)}, + }, + ).Logf("adjusted partitions' max size") + } +} + +func (c *Cache) start() func() { + ticker := time.NewTicker(c.opts.CheckInterval) + quit := make(chan struct{}) + + go func() { + for { + select { + case <-ticker.C: + c.adjustCurrentMaxSize() + // Reset the ticker to avoid drift. + ticker.Reset(c.opts.CheckInterval) + case <-quit: + ticker.Stop() + return + } + } + }() + + return func() { + close(quit) + } +} + +var partitionNameRe = regexp.MustCompile(`^\/[a-zA-Z0-9]{4}(\/[a-zA-Z0-9]+)?(\/[a-zA-Z0-9]+)?`) + +// GetOrCreatePartition gets or creates a partition with the given name. +func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts OptionsPartition) *Partition[K, V] { + if c == nil { + panic("nil Cache") + } + if opts.Weight < 1 || opts.Weight > 100 { + panic("invalid Weight, must be between 1 and 100") + } + + if partitionNameRe.FindString(name) != name { + panic(fmt.Sprintf("invalid partition name %q", name)) + } + + c.mu.RLock() + p, found := c.partitions[name] + c.mu.RUnlock() + if found { + return p.(*Partition[K, V]) + } + + c.mu.Lock() + defer c.mu.Unlock() + + // Double check. + p, found = c.partitions[name] + if found { + return p.(*Partition[K, V]) + } + + // At this point, we don't know the number of partitions or their configuration, but + // this will be re-adjusted later. + const numberOfPartitionsEstimate = 10 + maxSize := opts.CalculateMaxSize(c.opts.MaxSize / numberOfPartitionsEstimate) + + onEvict := func(k K, v V) { + c.onEvict(k, v) + } + + // Create a new partition and cache it. + partition := &Partition[K, V]{ + c: lazycache.New(lazycache.Options[K, V]{MaxEntries: maxSize, OnEvict: onEvict}), + maxSize: maxSize, + trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name), + opts: opts, + } + + c.partitions[name] = partition + + return partition +} + +// Partition is a partition in the cache. +type Partition[K comparable, V any] struct { + c *lazycache.Cache[K, V] + + zero V + + trace logg.LevelLogger + opts OptionsPartition + + maxSize int +} + +// GetOrCreate gets or creates a value for the given key. +func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) { + v, err := p.doGetOrCreate(key, create) + if err != nil { + return p.zero, err + } + if resource.StaleVersion(v) > 0 { + p.c.Delete(key) + return p.doGetOrCreate(key, create) + } + return v, err +} + +func (p *Partition[K, V]) doGetOrCreate(key K, create func(key K) (V, error)) (V, error) { + v, _, err := p.c.GetOrCreate(key, create) + return v, err +} + +func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) { + v, err := p.doGetOrCreateWitTimeout(key, duration, create) + if err != nil { + return p.zero, err + } + if resource.StaleVersion(v) > 0 { + p.c.Delete(key) + return p.doGetOrCreateWitTimeout(key, duration, create) + } + return v, err +} + +// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function +// takes too long. +func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) { + resultch := make(chan V, 1) + errch := make(chan error, 1) + + go func() { + var ( + v V + err error + ) + defer func() { + if r := recover(); r != nil { + if rerr, ok := r.(error); ok { + err = rerr + } else { + err = fmt.Errorf("panic: %v", r) + } + } + if err != nil { + errch <- err + } else { + resultch <- v + } + }() + v, _, err = p.c.GetOrCreate(key, create) + }() + + select { + case v := <-resultch: + return v, nil + case err := <-errch: + return p.zero, err + case <-time.After(duration): + return p.zero, &herrors.TimeoutError{ + Duration: duration, + } + } +} + +func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) { + p.c.DeleteFunc(func(key K, v V) bool { + if predicate(key, v) { + p.trace.Log( + logg.StringFunc( + func() string { + return fmt.Sprintf("clearing cache key %v", key) + }, + ), + ) + return true + } + return false + }) +} + +func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) { + if predicate == nil { + predicate = func(k, v any) bool { + return false + } + } + opts := p.getOptions() + if opts.ClearWhen == ClearNever { + return + } + + if opts.ClearWhen == ClearOnRebuild { + // Clear all. + p.Clear() + return + } + + depsFinder := identity.NewFinder(identity.FinderConfig{}) + + shouldDelete := func(key K, v V) bool { + // We always clear elements marked as stale. + if resource.StaleVersion(v) > 0 { + return true + } + + // Now check if this entry has changed based on the changeset + // based on filesystem events. + if len(changeset) == 0 { + // Nothing changed. + return false + } + + var probablyDependent bool + identity.WalkIdentitiesShallow(v, func(level int, id2 identity.Identity) bool { + for _, id := range changeset { + if r := depsFinder.Contains(id, id2, -1); r > 0 { + // It's probably dependent, evict from cache. + probablyDependent = true + return true + } + } + return false + }) + + return probablyDependent + } + + // First pass. + // Second pass needs to be done in a separate loop to catch any + // elements marked as stale in the other partitions. + p.c.DeleteFunc(func(key K, v V) bool { + if predicate(key, v) || shouldDelete(key, v) { + p.trace.Log( + logg.StringFunc( + func() string { + return fmt.Sprintf("first pass: clearing cache key %v", key) + }, + ), + ) + return true + } + return false + }) +} + +func (p *Partition[K, V]) Keys() []K { + var keys []K + p.c.DeleteFunc(func(key K, v V) bool { + keys = append(keys, key) + return false + }) + return keys +} + +func (p *Partition[K, V]) clearStale() { + p.c.DeleteFunc(func(key K, v V) bool { + staleVersion := resource.StaleVersion(v) + if staleVersion > 0 { + p.trace.Log( + logg.StringFunc( + func() string { + return fmt.Sprintf("second pass: clearing cache key %v", key) + }, + ), + ) + } + + return staleVersion > 0 + }) +} + +// adjustMaxSize adjusts the max size of the and returns the number of items evicted. +func (p *Partition[K, V]) adjustMaxSize(newMaxSize int) int { + if newMaxSize < minMaxSize { + newMaxSize = minMaxSize + } + oldMaxSize := p.maxSize + if newMaxSize == oldMaxSize { + return 0 + } + p.maxSize = newMaxSize + // fmt.Println("Adjusting max size of partition from", oldMaxSize, "to", newMaxSize) + return p.c.Resize(newMaxSize) +} + +func (p *Partition[K, V]) getMaxSize() int { + return p.maxSize +} + +func (p *Partition[K, V]) getOptions() OptionsPartition { + return p.opts +} + +func (p *Partition[K, V]) Clear() { + p.c.DeleteFunc(func(key K, v V) bool { + return true + }) +} + +func (p *Partition[K, V]) Get(ctx context.Context, key K) (V, bool) { + return p.c.Get(key) +} + +type PartitionManager interface { + adjustMaxSize(addend int) int + getMaxSize() int + getOptions() OptionsPartition + clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) + clearMatching(predicate func(k, v any) bool) + clearStale() +} + +const ( + ClearOnRebuild ClearWhen = iota + 1 + ClearOnChange + ClearNever +) + +type ClearWhen int + +type stats struct { + opts Options + memstatsCurrent runtime.MemStats + currentMaxSize int + availableMemory uint64 + + adjustmentFactor float64 +} + +func (s *stats) adjustCurrentMaxSize() bool { + newCurrentMaxSize := int(math.Floor(float64(s.opts.MaxSize) * s.adjustmentFactor)) + + if newCurrentMaxSize < s.opts.MinMaxSize { + newCurrentMaxSize = int(s.opts.MinMaxSize) + } + changed := newCurrentMaxSize != s.currentMaxSize + s.currentMaxSize = newCurrentMaxSize + return changed +} + +// CleanKey turns s into a format suitable for a cache key for this package. +// The key will be a Unix-styled path with a leading slash but no trailing slash. +func CleanKey(s string) string { + return path.Clean(paths.ToSlashPreserveLeading(s)) +} diff --git a/cache/dynacache/dynacache_test.go b/cache/dynacache/dynacache_test.go new file mode 100644 index 000000000..78b2fc82e --- /dev/null +++ b/cache/dynacache/dynacache_test.go @@ -0,0 +1,230 @@ +// Copyright 2024 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 dynacache + +import ( + "errors" + "fmt" + "path/filepath" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/resources/resource" +) + +var ( + _ resource.StaleInfo = (*testItem)(nil) + _ identity.Identity = (*testItem)(nil) +) + +type testItem struct { + name string + staleVersion uint32 +} + +func (t testItem) StaleVersion() uint32 { + return t.staleVersion +} + +func (t testItem) IdentifierBase() string { + return t.name +} + +func TestCache(t *testing.T) { + t.Parallel() + c := qt.New(t) + + cache := New(Options{ + Log: loggers.NewDefault(), + }) + + c.Cleanup(func() { + cache.Stop() + }) + + opts := OptionsPartition{Weight: 30} + + c.Assert(cache, qt.Not(qt.IsNil)) + + p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts) + c.Assert(p1, qt.Not(qt.IsNil)) + + p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", opts) + + c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "foo bar", opts) }, qt.PanicMatches, ".*invalid partition name.*") + c.Assert(func() { GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 1234}) }, qt.PanicMatches, ".*invalid Weight.*") + + c.Assert(p2, qt.Equals, p1) + + p3 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", opts) + c.Assert(p3, qt.Not(qt.IsNil)) + c.Assert(p3, qt.Not(qt.Equals), p1) + + c.Assert(func() { New(Options{}) }, qt.PanicMatches, ".*nil Log.*") +} + +func TestCalculateMaxSizePerPartition(t *testing.T) { + t.Parallel() + c := qt.New(t) + + c.Assert(calculateMaxSizePerPartition(1000, 500, 5), qt.Equals, 200) + c.Assert(calculateMaxSizePerPartition(1000, 250, 5), qt.Equals, 400) + c.Assert(func() { calculateMaxSizePerPartition(1000, 250, 0) }, qt.PanicMatches, ".*must be > 0.*") + c.Assert(func() { calculateMaxSizePerPartition(1000, 0, 1) }, qt.PanicMatches, ".*must be > 0.*") +} + +func TestCleanKey(t *testing.T) { + c := qt.New(t) + + c.Assert(CleanKey("a/b/c"), qt.Equals, "/a/b/c") + c.Assert(CleanKey("/a/b/c"), qt.Equals, "/a/b/c") + c.Assert(CleanKey("a/b/c/"), qt.Equals, "/a/b/c") + c.Assert(CleanKey(filepath.FromSlash("/a/b/c/")), qt.Equals, "/a/b/c") +} + +func newTestCache(t *testing.T) *Cache { + cache := New( + Options{ + Log: loggers.NewDefault(), + }, + ) + + p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild}) + p2 := GetOrCreatePartition[string, testItem](cache, "/aaaa/cccc", OptionsPartition{Weight: 30, ClearWhen: ClearOnChange}) + + p1.GetOrCreate("clearOnRebuild", func(string) (testItem, error) { + return testItem{}, nil + }) + + p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) { + return testItem{ + staleVersion: 32, + }, nil + }) + + p2.GetOrCreate("clearBecauseIdentityChanged", func(string) (testItem, error) { + return testItem{ + name: "changed", + }, nil + }) + + p2.GetOrCreate("clearNever", func(string) (testItem, error) { + return testItem{ + staleVersion: 0, + }, nil + }) + + t.Cleanup(func() { + cache.Stop() + }) + + return cache +} + +func TestClear(t *testing.T) { + t.Parallel() + c := qt.New(t) + + predicateAll := func(string) bool { + return true + } + + cache := newTestCache(t) + + c.Assert(cache.Keys(predicateAll), qt.HasLen, 4) + + cache.ClearOnRebuild(nil) + + // Stale items are always cleared. + c.Assert(cache.Keys(predicateAll), qt.HasLen, 2) + + cache = newTestCache(t) + cache.ClearOnRebuild(nil, identity.StringIdentity("changed")) + + c.Assert(cache.Keys(nil), qt.HasLen, 1) + + cache = newTestCache(t) + + cache.ClearMatching(nil, func(k, v any) bool { + return k.(string) == "clearOnRebuild" + }) + + c.Assert(cache.Keys(predicateAll), qt.HasLen, 3) + + cache.adjustCurrentMaxSize() +} + +func TestPanicInCreate(t *testing.T) { + t.Parallel() + c := qt.New(t) + cache := newTestCache(t) + + p1 := GetOrCreatePartition[string, testItem](cache, "/aaaa/bbbb", OptionsPartition{Weight: 30, ClearWhen: ClearOnRebuild}) + + willPanic := func(i int) func() { + return func() { + p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) { + panic(errors.New(key)) + }) + } + } + + // GetOrCreateWitTimeout needs to recover from panics in the create func. + willErr := func(i int) error { + _, err := p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) { + return testItem{}, errors.New(key) + }) + return err + } + + for i := range 3 { + for range 3 { + c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i)) + c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i)) + } + } + + // Test the same keys again without the panic. + for i := range 3 { + for range 3 { + v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) { + return testItem{ + name: key, + }, nil + }) + c.Assert(err, qt.IsNil) + c.Assert(v.name, qt.Equals, fmt.Sprintf("panic-%d", i)) + + v, err = p1.GetOrCreateWitTimeout(fmt.Sprintf("error-%d", i), 10*time.Second, func(key string) (testItem, error) { + return testItem{ + name: key, + }, nil + }) + c.Assert(err, qt.IsNil) + c.Assert(v.name, qt.Equals, fmt.Sprintf("error-%d", i)) + } + } +} + +func TestAdjustCurrentMaxSize(t *testing.T) { + t.Parallel() + c := qt.New(t) + cache := newTestCache(t) + alloc := cache.stats.memstatsCurrent.Alloc + cache.adjustCurrentMaxSize() + c.Assert(cache.stats.memstatsCurrent.Alloc, qt.Not(qt.Equals), alloc) +} diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go index 05d9379b4..01c466ca6 100644 --- a/cache/filecache/filecache.go +++ b/cache/filecache/filecache.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -23,7 +23,9 @@ import ( "sync" "time" + "github.com/gohugoio/httpcache" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/helpers" @@ -51,6 +53,9 @@ type Cache struct { pruneAllRootDir string nlocker *lockTracker + + initOnce sync.Once + initErr error } type lockTracker struct { @@ -103,9 +108,23 @@ func (l *lockedFile) Close() error { return l.File.Close() } +func (c *Cache) init() error { + c.initOnce.Do(func() { + // Create the base dir if it does not exist. + if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) { + c.initErr = err + } + }) + return c.initErr +} + // WriteCloser returns a transactional writer into the cache. // It's important that it's closed when done. func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) { + if err := c.init(); err != nil { + return ItemInfo{}, nil, err + } + id = cleanID(id) c.nlocker.Lock(id) @@ -129,7 +148,12 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) { // it when done. func (c *Cache) ReadOrCreate(id string, read func(info ItemInfo, r io.ReadSeeker) error, - create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) { + create func(info ItemInfo, w io.WriteCloser) error, +) (info ItemInfo, err error) { + if err := c.init(); err != nil { + return ItemInfo{}, err + } + id = cleanID(id) c.nlocker.Lock(id) @@ -159,10 +183,22 @@ func (c *Cache) ReadOrCreate(id string, return } +// NamedLock locks the given id. The lock is released when the returned function is called. +func (c *Cache) NamedLock(id string) func() { + id = cleanID(id) + c.nlocker.Lock(id) + return func() { + c.nlocker.Unlock(id) + } +} + // GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will // be invoked and the result cached. // This method is protected by a named lock using the given id as identifier. func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) { + if err := c.init(); err != nil { + return ItemInfo{}, nil, err + } id = cleanID(id) c.nlocker.Lock(id) @@ -192,11 +228,30 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It var buff bytes.Buffer return info, hugio.ToReadCloser(&buff), - afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff)) + c.writeReader(id, io.TeeReader(r, &buff)) +} + +func (c *Cache) writeReader(id string, r io.Reader) error { + dir := filepath.Dir(id) + if dir != "" { + _ = c.Fs.MkdirAll(dir, 0o777) + } + f, err := c.Fs.Create(id) + if err != nil { + return err + } + defer f.Close() + + _, _ = io.Copy(f, r) + + return nil } // GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice. func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) { + if err := c.init(); err != nil { + return ItemInfo{}, nil, err + } id = cleanID(id) c.nlocker.Lock(id) @@ -224,14 +279,18 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item return info, b, nil } - if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil { + if err := c.writeReader(id, bytes.NewReader(b)); err != nil { return info, nil, err } + return info, b, nil } // GetBytes gets the file content with the given id from the cache, nil if none found. func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) { + if err := c.init(); err != nil { + return ItemInfo{}, nil, err + } id = cleanID(id) c.nlocker.Lock(id) @@ -250,6 +309,9 @@ func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) { // Get gets the file with the given id from the cache, nil if none found. func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) { + if err := c.init(); err != nil { + return ItemInfo{}, nil, err + } id = cleanID(id) c.nlocker.Lock(id) @@ -270,16 +332,8 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser { return nil } - if c.maxAge > 0 { - fi, err := c.Fs.Stat(id) - if err != nil { - return nil - } - - if c.isExpired(fi.ModTime()) { - c.Fs.Remove(id) - return nil - } + if removed, err := c.removeIfExpired(id); err != nil || removed { + return nil } f, err := c.Fs.Open(id) @@ -290,6 +344,49 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser { return f } +func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) { + if c.maxAge == 0 { + // No caching. + return nil, false + } + + f, err := c.Fs.Open(id) + if err != nil { + return nil, false + } + defer f.Close() + + b, err := io.ReadAll(f) + if err != nil { + return nil, false + } + + removed, err := c.removeIfExpired(id) + if err != nil { + return nil, false + } + + return b, removed +} + +func (c *Cache) removeIfExpired(id string) (bool, error) { + if c.maxAge <= 0 { + return false, nil + } + + fi, err := c.Fs.Stat(id) + if err != nil { + return false, err + } + + if c.isExpired(fi.ModTime()) { + c.Fs.Remove(id) + return true, nil + } + + return false, nil +} + func (c *Cache) isExpired(modTime time.Time) bool { if c.maxAge < 0 { return false @@ -347,11 +444,7 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) { baseDir := v.DirCompiled - if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) { - return nil, err - } - - bfs := afero.NewBasePathFs(cfs, baseDir) + bfs := hugofs.NewBasePathFs(cfs, baseDir) var pruneAllRootDir string if k == CacheKeyModules { @@ -367,3 +460,37 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) { func cleanID(name string) string { return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator) } + +// AsHTTPCache returns an httpcache.Cache implementation for this file cache. +// Note that none of the methods are protected by named locks, so you need to make sure +// to do that in your own code. +func (c *Cache) AsHTTPCache() httpcache.Cache { + return &httpCache{c: c} +} + +type httpCache struct { + c *Cache +} + +func (h *httpCache) Get(id string) (resp []byte, ok bool) { + id = cleanID(id) + b, removed := h.c.getBytesAndRemoveIfExpired(id) + + return b, !removed +} + +func (h *httpCache) Set(id string, resp []byte) { + if h.c.maxAge == 0 { + return + } + + id = cleanID(id) + + if err := h.c.writeReader(id, bytes.NewReader(resp)); err != nil { + panic(err) + } +} + +func (h *httpCache) Delete(key string) { + h.c.Fs.Remove(key) +} diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go index e8019578a..a71ddb474 100644 --- a/cache/filecache/filecache_config.go +++ b/cache/filecache/filecache_config.go @@ -15,6 +15,7 @@ package filecache import ( + "errors" "fmt" "path" "path/filepath" @@ -24,8 +25,6 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" - "errors" - "github.com/mitchellh/mapstructure" "github.com/spf13/afero" ) @@ -47,6 +46,7 @@ const ( CacheKeyAssets = "assets" CacheKeyModules = "modules" CacheKeyGetResource = "getresource" + CacheKeyMisc = "misc" ) type Configs map[string]FileCacheConfig @@ -71,17 +71,21 @@ var defaultCacheConfigs = Configs{ MaxAge: -1, Dir: resourcesGenDir, }, - CacheKeyGetResource: FileCacheConfig{ + CacheKeyGetResource: { MaxAge: -1, // Never expire Dir: cacheDirProject, }, + CacheKeyMisc: { + MaxAge: -1, + Dir: cacheDirProject, + }, } type FileCacheConfig struct { // Max age of cache entries in this cache. Any items older than this will // be removed and not returned from the cache. // A negative value means forever, 0 means cache is disabled. - // Hugo is leninent with what types it accepts here, but we recommend using + // Hugo is lenient with what types it accepts here, but we recommend using // a duration string, a sequence of decimal numbers, each with optional fraction and a unit suffix, // such as "300ms", "1.5h" or "2h45m". // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". @@ -93,7 +97,7 @@ type FileCacheConfig struct { // Will resources/_gen will get its own composite filesystem that // also checks any theme. - IsResourceDir bool + IsResourceDir bool `json:"-"` } // GetJSONCache gets the file cache for getJSON. @@ -121,6 +125,11 @@ func (f Caches) AssetsCache() *Cache { return f[CacheKeyAssets] } +// MiscCache gets the file cache for miscellaneous stuff. +func (f Caches) MiscCache() *Cache { + return f[CacheKeyMisc] +} + // GetResourceCache gets the file cache for remote resources. func (f Caches) GetResourceCache() *Cache { return f[CacheKeyGetResource] @@ -225,7 +234,6 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config // Resolves :resourceDir => /myproject/resources etc., :cacheDir => ... func resolveDirPlaceholder(fs afero.Fs, bcfg config.BaseConfig, placeholder string) (cacheDir string, isResource bool, err error) { - switch strings.ToLower(placeholder) { case ":resourcedir": return "", true, nil diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go index f93c7060e..c6d346dfc 100644 --- a/cache/filecache/filecache_config_test.go +++ b/cache/filecache/filecache_config_test.go @@ -59,7 +59,7 @@ dir = "/path/to/c4" c.Assert(err, qt.IsNil) fs := afero.NewMemMapFs() decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 6) + c.Assert(len(decoded), qt.Equals, 7) c2 := decoded["getcsv"] c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s") @@ -106,7 +106,7 @@ dir = "/path/to/c4" c.Assert(err, qt.IsNil) fs := afero.NewMemMapFs() decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 6) + c.Assert(len(decoded), qt.Equals, 7) for _, v := range decoded { c.Assert(v.MaxAge, qt.Equals, time.Duration(0)) @@ -129,7 +129,7 @@ func TestDecodeConfigDefault(t *testing.T) { fs := afero.NewMemMapFs() decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches - c.Assert(len(decoded), qt.Equals, 6) + c.Assert(len(decoded), qt.Equals, 7) imgConfig := decoded[filecache.CacheKeyImages] jsonConfig := decoded[filecache.CacheKeyGetJSON] diff --git a/cache/filecache/filecache_integration_test.go b/cache/filecache/filecache_integration_test.go new file mode 100644 index 000000000..1e920c29f --- /dev/null +++ b/cache/filecache/filecache_integration_test.go @@ -0,0 +1,106 @@ +// Copyright 2024 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 filecache_test + +import ( + "path/filepath" + "testing" + "time" + + "github.com/bep/logg" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/htesting" + "github.com/gohugoio/hugo/hugolib" +) + +// See issue #10781. That issue wouldn't have been triggered if we kept +// the empty root directories (e.g. _resources/gen/images). +// It's still an upstream Go issue that we also need to handle, but +// this is a test for the first part. +func TestPruneShouldPreserveEmptyCacheRoots(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +-- content/_index.md -- +--- +title: "Home" +--- + +` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true}, + ).Build() + + _, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images")) + + b.Assert(err, qt.IsNil) +} + +func TestPruneImages(t *testing.T) { + if htesting.IsCI() { + // TODO(bep) + t.Skip("skip flaky test on CI server") + } + t.Skip("skip flaky test") + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[caches] +[caches.images] +maxAge = "200ms" +dir = ":resourceDir/_gen" +-- content/_index.md -- +--- +title: "Home" +--- +-- assets/a/pixel.png -- +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== +-- layouts/index.html -- +{{ warnf "HOME!" }} +{{ $img := resources.GetMatch "**.png" }} +{{ $img = $img.Resize "3x3" }} +{{ $img.RelPermalink }} + + + +` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: logg.LevelInfo}, + ).Build() + + b.Assert(b.GCCount, qt.Equals, 0) + b.Assert(b.H, qt.IsNotNil) + + imagesCacheDir := filepath.Join("_gen", "images") + _, err := b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir) + + b.Assert(err, qt.IsNil) + + // TODO(bep) we need a way to test full rebuilds. + // For now, just sleep a little so the cache elements expires. + time.Sleep(500 * time.Millisecond) + + b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build() + + b.Assert(b.GCCount, qt.Equals, 1) + // Build it again to GC the empty a dir. + b.Build() + + _, err = b.H.BaseFs.ResourcesCache.Stat(filepath.Join(imagesCacheDir, "a")) + b.Assert(err, qt.Not(qt.IsNil)) + _, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir) + b.Assert(err, qt.IsNil) +} diff --git a/cache/filecache/filecache_pruner.go b/cache/filecache/filecache_pruner.go index e1b7f1947..6f224cef4 100644 --- a/cache/filecache/filecache_pruner.go +++ b/cache/filecache/filecache_pruner.go @@ -53,11 +53,13 @@ func (c *Cache) Prune(force bool) (int, error) { if c.pruneAllRootDir != "" { return c.pruneRootDir(force) } + if err := c.init(); err != nil { + return 0, err + } counter := 0 err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error { - if info == nil { return nil } @@ -66,7 +68,6 @@ func (c *Cache) Prune(force bool) (int, error) { if info.IsDir() { f, err := c.Fs.Open(name) - if err != nil { // This cache dir may not exist. return nil @@ -117,6 +118,9 @@ func (c *Cache) Prune(force bool) (int, error) { } func (c *Cache) pruneRootDir(force bool) (int, error) { + if err := c.init(); err != nil { + return 0, err + } info, err := c.Fs.Stat(c.pruneAllRootDir) if err != nil { if herrors.IsNotExist(err) { diff --git a/cache/filecache/filecache_pruner_test.go b/cache/filecache/filecache_pruner_test.go index f0cecfe9f..b49ba7645 100644 --- a/cache/filecache/filecache_pruner_test.go +++ b/cache/filecache/filecache_pruner_test.go @@ -59,7 +59,7 @@ dir = ":resourceDir/_gen" caches, err := filecache.NewCaches(p) c.Assert(err, qt.IsNil) cache := caches[name] - for i := 0; i < 10; i++ { + for i := range 10 { id := fmt.Sprintf("i%d", i) cache.GetOrCreateBytes(id, func() ([]byte, error) { return []byte("abc"), nil @@ -74,7 +74,7 @@ dir = ":resourceDir/_gen" c.Assert(err, qt.IsNil) c.Assert(count, qt.Equals, 5, msg) - for i := 0; i < 10; i++ { + for i := range 10 { id := fmt.Sprintf("i%d", i) v := cache.GetString(id) if i < 5 { @@ -97,7 +97,7 @@ dir = ":resourceDir/_gen" c.Assert(count, qt.Equals, 4) // Now only the i5 should be left. - for i := 0; i < 10; i++ { + for i := range 10 { id := fmt.Sprintf("i%d", i) v := cache.GetString(id) if i != 5 { diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go index 61f9eda64..a30aaa50b 100644 --- a/cache/filecache/filecache_test.go +++ b/cache/filecache/filecache_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -17,7 +17,6 @@ import ( "errors" "fmt" "io" - "path/filepath" "strings" "sync" "testing" @@ -86,17 +85,8 @@ dir = ":cacheDir/c" cache := caches.Get("GetJSON") c.Assert(cache, qt.Not(qt.IsNil)) - bfs, ok := cache.Fs.(*afero.BasePathFs) - c.Assert(ok, qt.Equals, true) - filename, err := bfs.RealPath("key") - c.Assert(err, qt.IsNil) - cache = caches.Get("Images") c.Assert(cache, qt.Not(qt.IsNil)) - bfs, ok = cache.Fs.(*afero.BasePathFs) - c.Assert(ok, qt.Equals, true) - filename, _ = bfs.RealPath("key") - c.Assert(filename, qt.Equals, filepath.FromSlash("_gen/images/key")) rf := func(s string) func() (io.ReadCloser, error) { return func() (io.ReadCloser, error) { @@ -115,7 +105,7 @@ dir = ":cacheDir/c" } for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} { - for i := 0; i < 2; i++ { + for range 2 { info, r, err := ca.GetOrCreate("a", rf("abc")) c.Assert(err, qt.IsNil) c.Assert(r, qt.Not(qt.IsNil)) @@ -203,11 +193,11 @@ dir = "/cache/c" var wg sync.WaitGroup - for i := 0; i < 50; i++ { + for i := range 50 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 20; j++ { + for range 20 { ca := caches.Get(cacheName) c.Assert(ca, qt.Not(qt.IsNil)) filename, data := filenameData(i) diff --git a/cache/filecache/integration_test.go b/cache/filecache/integration_test.go deleted file mode 100644 index a59ea048d..000000000 --- a/cache/filecache/integration_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// 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 filecache_test - -import ( - "path/filepath" - - jww "github.com/spf13/jwalterweatherman" - - "testing" - "time" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/hugolib" -) - -// See issue #10781. That issue wouldn't have been triggered if we kept -// the empty root directories (e.g. _resources/gen/images). -// It's still an upstream Go issue that we also need to handle, but -// this is a test for the first part. -func TestPruneShouldPreserveEmptyCacheRoots(t *testing.T) { - files := ` --- hugo.toml -- -baseURL = "https://example.com" --- content/_index.md -- ---- -title: "Home" ---- - -` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true}, - ).Build() - - _, err := b.H.BaseFs.ResourcesCache.Stat(filepath.Join("_gen", "images")) - - b.Assert(err, qt.IsNil) - -} - -func TestPruneImages(t *testing.T) { - if htesting.IsCI() { - // TODO(bep) - t.Skip("skip flaky test on CI server") - } - files := ` --- hugo.toml -- -baseURL = "https://example.com" -[caches] -[caches.images] -maxAge = "200ms" -dir = ":resourceDir/_gen" --- content/_index.md -- ---- -title: "Home" ---- --- assets/a/pixel.png -- -iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== --- layouts/index.html -- -{{ warnf "HOME!" }} -{{ $img := resources.GetMatch "**.png" }} -{{ $img = $img.Resize "3x3" }} -{{ $img.RelPermalink }} - - - -` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: jww.LevelInfo}, - ).Build() - - b.Assert(b.GCCount, qt.Equals, 0) - b.Assert(b.H, qt.IsNotNil) - - imagesCacheDir := filepath.Join("_gen", "images") - _, err := b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir) - - b.Assert(err, qt.IsNil) - - // TODO(bep) we need a way to test full rebuilds. - // For now, just sleep a little so the cache elements expires. - time.Sleep(300 * time.Millisecond) - - b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build() - - b.Assert(b.GCCount, qt.Equals, 1) - // Build it again to GC the empty a dir. - b.Build() - - _, err = b.H.BaseFs.ResourcesCache.Stat(filepath.Join(imagesCacheDir, "a")) - b.Assert(err, qt.Not(qt.IsNil)) - _, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir) - b.Assert(err, qt.IsNil) - -} diff --git a/cache/httpcache/httpcache.go b/cache/httpcache/httpcache.go new file mode 100644 index 000000000..bd6d4bf7d --- /dev/null +++ b/cache/httpcache/httpcache.go @@ -0,0 +1,229 @@ +// Copyright 2024 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 httpcache + +import ( + "encoding/json" + "time" + + "github.com/gobwas/glob" + "github.com/gohugoio/hugo/common/predicate" + "github.com/gohugoio/hugo/config" + "github.com/mitchellh/mapstructure" +) + +// DefaultConfig holds the default configuration for the HTTP cache. +var DefaultConfig = Config{ + Cache: Cache{ + For: GlobMatcher{ + Excludes: []string{"**"}, + }, + }, + Polls: []PollConfig{ + { + For: GlobMatcher{ + Includes: []string{"**"}, + }, + Disable: true, + }, + }, +} + +// Config holds the configuration for the HTTP cache. +type Config struct { + // Configures the HTTP cache behavior (RFC 9111). + // When this is not enabled for a resource, Hugo will go straight to the file cache. + Cache Cache + + // Polls holds a list of configurations for polling remote resources to detect changes in watch mode. + // This can be disabled for some resources, typically if they are known to not change. + Polls []PollConfig +} + +type Cache struct { + // Enable HTTP cache behavior (RFC 9111) for these resources. + For GlobMatcher +} + +func (c *Config) Compile() (ConfigCompiled, error) { + var cc ConfigCompiled + + p, err := c.Cache.For.CompilePredicate() + if err != nil { + return cc, err + } + + cc.For = p + + for _, pc := range c.Polls { + + p, err := pc.For.CompilePredicate() + if err != nil { + return cc, err + } + + cc.PollConfigs = append(cc.PollConfigs, PollConfigCompiled{ + For: p, + Config: pc, + }) + } + + return cc, nil +} + +// PollConfig holds the configuration for polling remote resources to detect changes in watch mode. +type PollConfig struct { + // What remote resources to apply this configuration to. + For GlobMatcher + + // Disable polling for this configuration. + Disable bool + + // Low is the lower bound for the polling interval. + // This is the starting point when the resource has recently changed, + // if that resource stops changing, the polling interval will gradually increase towards High. + Low time.Duration + + // High is the upper bound for the polling interval. + // This is the interval used when the resource is stable. + High time.Duration +} + +func (c PollConfig) MarshalJSON() (b []byte, err error) { + // Marshal the durations as strings. + type Alias PollConfig + return json.Marshal(&struct { + Low string + High string + Alias + }{ + Low: c.Low.String(), + High: c.High.String(), + Alias: (Alias)(c), + }) +} + +type GlobMatcher struct { + // Excludes holds a list of glob patterns that will be excluded. + Excludes []string + + // Includes holds a list of glob patterns that will be included. + Includes []string +} + +func (gm GlobMatcher) IsZero() bool { + return len(gm.Includes) == 0 && len(gm.Excludes) == 0 +} + +type ConfigCompiled struct { + For predicate.P[string] + PollConfigs []PollConfigCompiled +} + +func (c *ConfigCompiled) PollConfigFor(s string) PollConfigCompiled { + for _, pc := range c.PollConfigs { + if pc.For(s) { + return pc + } + } + return PollConfigCompiled{} +} + +func (c *ConfigCompiled) IsPollingDisabled() bool { + for _, pc := range c.PollConfigs { + if !pc.Config.Disable { + return false + } + } + return true +} + +type PollConfigCompiled struct { + For predicate.P[string] + Config PollConfig +} + +func (p PollConfigCompiled) IsZero() bool { + return p.For == nil +} + +func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) { + if gm.IsZero() { + panic("no includes or excludes") + } + var p predicate.P[string] + for _, include := range gm.Includes { + g, err := glob.Compile(include, '/') + if err != nil { + return nil, err + } + fn := func(s string) bool { + return g.Match(s) + } + p = p.Or(fn) + } + + for _, exclude := range gm.Excludes { + g, err := glob.Compile(exclude, '/') + if err != nil { + return nil, err + } + fn := func(s string) bool { + return !g.Match(s) + } + p = p.And(fn) + } + + return p, nil +} + +func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) { + if len(m) == 0 { + return DefaultConfig, nil + } + + var c Config + + dc := &mapstructure.DecoderConfig{ + Result: &c, + DecodeHook: mapstructure.StringToTimeDurationHookFunc(), + WeaklyTypedInput: true, + } + + decoder, err := mapstructure.NewDecoder(dc) + if err != nil { + return c, err + } + + if err := decoder.Decode(m); err != nil { + return c, err + } + + if c.Cache.For.IsZero() { + c.Cache.For = DefaultConfig.Cache.For + } + + for pci := range c.Polls { + if c.Polls[pci].For.IsZero() { + c.Polls[pci].For = DefaultConfig.Cache.For + c.Polls[pci].Disable = true + } + } + + if len(c.Polls) == 0 { + c.Polls = DefaultConfig.Polls + } + + return c, nil +} diff --git a/cache/httpcache/httpcache_integration_test.go b/cache/httpcache/httpcache_integration_test.go new file mode 100644 index 000000000..4d6a5f718 --- /dev/null +++ b/cache/httpcache/httpcache_integration_test.go @@ -0,0 +1,95 @@ +// Copyright 2024 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 httpcache_test + +import ( + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/hugolib" +) + +func TestConfigCustom(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +[httpcache] +[httpcache.cache.for] +includes = ["**gohugo.io**"] +[[httpcache.polls]] +low = "5s" +high = "32s" +[httpcache.polls.for] +includes = ["**gohugo.io**"] + + +` + + b := hugolib.Test(t, files) + + httpcacheConf := b.H.Configs.Base.HTTPCache + compiled := b.H.Configs.Base.C.HTTPCache + + b.Assert(httpcacheConf.Cache.For.Includes, qt.DeepEquals, []string{"**gohugo.io**"}) + b.Assert(httpcacheConf.Cache.For.Excludes, qt.IsNil) + + pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg") + b.Assert(pc.Config.Low, qt.Equals, 5*time.Second) + b.Assert(pc.Config.High, qt.Equals, 32*time.Second) + b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue) +} + +func TestConfigDefault(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +` + b := hugolib.Test(t, files) + + compiled := b.H.Configs.Base.C.HTTPCache + + b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse) + b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse) + b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue) +} + +func TestConfigPollsOnly(t *testing.T) { + t.Parallel() + files := ` +-- hugo.toml -- +[httpcache] +[[httpcache.polls]] +low = "5s" +high = "32s" +[httpcache.polls.for] +includes = ["**gohugo.io**"] + + +` + + b := hugolib.Test(t, files) + + compiled := b.H.Configs.Base.C.HTTPCache + + b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse) + b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse) + + pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg") + b.Assert(pc.Config.Low, qt.Equals, 5*time.Second) + b.Assert(pc.Config.High, qt.Equals, 32*time.Second) + b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue) +} diff --git a/cache/httpcache/httpcache_test.go b/cache/httpcache/httpcache_test.go new file mode 100644 index 000000000..60c07d056 --- /dev/null +++ b/cache/httpcache/httpcache_test.go @@ -0,0 +1,73 @@ +// Copyright 2024 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 httpcache + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" +) + +func TestGlobMatcher(t *testing.T) { + c := qt.New(t) + + g := GlobMatcher{ + Includes: []string{"**/*.jpg", "**.png", "**/bar/**"}, + Excludes: []string{"**/foo.jpg", "**.css"}, + } + + p, err := g.CompilePredicate() + c.Assert(err, qt.IsNil) + + c.Assert(p("foo.jpg"), qt.IsFalse) + c.Assert(p("foo.png"), qt.IsTrue) + c.Assert(p("foo/bar.jpg"), qt.IsTrue) + c.Assert(p("foo/bar.png"), qt.IsTrue) + c.Assert(p("foo/bar/foo.jpg"), qt.IsFalse) + c.Assert(p("foo/bar/foo.css"), qt.IsFalse) + c.Assert(p("foo.css"), qt.IsFalse) + c.Assert(p("foo/bar/foo.css"), qt.IsFalse) + c.Assert(p("foo/bar/foo.xml"), qt.IsTrue) +} + +func TestDefaultConfig(t *testing.T) { + c := qt.New(t) + + _, err := DefaultConfig.Compile() + c.Assert(err, qt.IsNil) +} + +func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) { + c := qt.New(t) + + cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{}) + c.Assert(err, qt.IsNil) + c.Assert(cfg, qt.DeepEquals, DefaultConfig) + + _, err = cfg.Compile() + c.Assert(err, qt.IsNil) + + cfg, err = DecodeConfig(config.BaseConfig{}, map[string]any{ + "cache": map[string]any{ + "polls": []map[string]any{ + {"disable": true}, + }, + }, + }) + c.Assert(err, qt.IsNil) + + _, err = cfg.Compile() + c.Assert(err, qt.IsNil) +} diff --git a/cache/namedmemcache/named_cache.go b/cache/namedmemcache/named_cache.go deleted file mode 100644 index 7fb4fe8ed..000000000 --- a/cache/namedmemcache/named_cache.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package namedmemcache provides a memory cache with a named lock. This is suitable -// for situations where creating the cached resource can be time consuming or otherwise -// resource hungry, or in situations where a "once only per key" is a requirement. -package namedmemcache - -import ( - "sync" - - "github.com/BurntSushi/locker" -) - -// Cache holds the cached values. -type Cache struct { - nlocker *locker.Locker - cache map[string]cacheEntry - mu sync.RWMutex -} - -type cacheEntry struct { - value any - err error -} - -// New creates a new cache. -func New() *Cache { - return &Cache{ - nlocker: locker.NewLocker(), - cache: make(map[string]cacheEntry), - } -} - -// Clear clears the cache state. -func (c *Cache) Clear() { - c.mu.Lock() - defer c.mu.Unlock() - - c.cache = make(map[string]cacheEntry) - c.nlocker = locker.NewLocker() -} - -// GetOrCreate tries to get the value with the given cache key, if not found -// create will be called and cached. -// This method is thread safe. It also guarantees that the create func for a given -// key is invoked only once for this cache. -func (c *Cache) GetOrCreate(key string, create func() (any, error)) (any, error) { - c.mu.RLock() - entry, found := c.cache[key] - c.mu.RUnlock() - - if found { - return entry.value, entry.err - } - - c.nlocker.Lock(key) - defer c.nlocker.Unlock(key) - - // Create it. - value, err := create() - - c.mu.Lock() - c.cache[key] = cacheEntry{value: value, err: err} - c.mu.Unlock() - - return value, err -} diff --git a/cache/namedmemcache/named_cache_test.go b/cache/namedmemcache/named_cache_test.go deleted file mode 100644 index 2db923d76..000000000 --- a/cache/namedmemcache/named_cache_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package namedmemcache - -import ( - "fmt" - "sync" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestNamedCache(t *testing.T) { - t.Parallel() - c := qt.New(t) - - cache := New() - - counter := 0 - create := func() (any, error) { - counter++ - return counter, nil - } - - for i := 0; i < 5; i++ { - v1, err := cache.GetOrCreate("a1", create) - c.Assert(err, qt.IsNil) - c.Assert(v1, qt.Equals, 1) - v2, err := cache.GetOrCreate("a2", create) - c.Assert(err, qt.IsNil) - c.Assert(v2, qt.Equals, 2) - } - - cache.Clear() - - v3, err := cache.GetOrCreate("a2", create) - c.Assert(err, qt.IsNil) - c.Assert(v3, qt.Equals, 3) -} - -func TestNamedCacheConcurrent(t *testing.T) { - t.Parallel() - - c := qt.New(t) - - var wg sync.WaitGroup - - cache := New() - - create := func(i int) func() (any, error) { - return func() (any, error) { - return i, nil - } - } - - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { - id := fmt.Sprintf("id%d", j) - v, err := cache.GetOrCreate(id, create(j)) - c.Assert(err, qt.IsNil) - c.Assert(v, qt.Equals, j) - } - }() - } - wg.Wait() -} diff --git a/check_gofmt.sh b/check_gofmt.sh new file mode 100755 index 000000000..c77517d3f --- /dev/null +++ b/check_gofmt.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +diff <(gofmt -d .) <(printf '') \ No newline at end of file diff --git a/codegen/methods.go b/codegen/methods.go index 65a7cc2b7..08ac97b00 100644 --- a/codegen/methods.go +++ b/codegen/methods.go @@ -26,6 +26,7 @@ import ( "path/filepath" "reflect" "regexp" + "slices" "sort" "strings" "sync" @@ -102,7 +103,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T } for _, t := range include { - for i := 0; i < t.NumMethod(); i++ { + for i := range t.NumMethod() { m := t.Method(i) if excludes[m.Name] || seen[m.Name] { @@ -122,7 +123,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T method := Method{Owner: t, OwnerName: ownerName, Name: m.Name} - for i := 0; i < numIn; i++ { + for i := range numIn { in := m.Type.In(i) name, pkg := nameAndPackage(in) @@ -137,7 +138,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T numOut := m.Type.NumOut() if numOut > 0 { - for i := 0; i < numOut; i++ { + for i := range numOut { out := m.Type.Out(i) name, pkg := nameAndPackage(out) @@ -304,7 +305,7 @@ func (m Method) inOutStr() string { } args := make([]string, len(m.In)) - for i := 0; i < len(args); i++ { + for i := range args { args[i] = fmt.Sprintf("arg%d", i) } return "(" + strings.Join(args, ", ") + ")" @@ -316,7 +317,7 @@ func (m Method) inStr() string { } args := make([]string, len(m.In)) - for i := 0; i < len(args); i++ { + for i := range args { args[i] = fmt.Sprintf("arg%d %s", i, m.In[i]) } return "(" + strings.Join(args, ", ") + ")" @@ -339,7 +340,7 @@ func (m Method) outStrNamed() string { } outs := make([]string, len(m.Out)) - for i := 0; i < len(outs); i++ { + for i := range outs { outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i]) } @@ -435,7 +436,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st // Exclude self for i, pkgImp := range pkgImports { if pkgImp == pkgPath { - pkgImports = append(pkgImports[:i], pkgImports[i+1:]...) + pkgImports = slices.Delete(pkgImports, i, i+1) } } } @@ -461,7 +462,6 @@ func collectMethodsRecursive(pkg string, f []*ast.Field) []string { pkg, tt.Methods.List)...) } - } else { // Embedded, but in a different file/package. Return the // package.Name and deal with that later. diff --git a/commands/commandeer.go b/commands/commandeer.go index 0ae6aefce..bf9655637 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -18,20 +18,22 @@ import ( "errors" "fmt" "io" + "log" "os" "os/signal" "path/filepath" + "runtime" + "strings" "sync" "sync/atomic" "syscall" "time" - jww "github.com/spf13/jwalterweatherman" - "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" "github.com/bep/simplecobra" @@ -39,19 +41,20 @@ import ( "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/resources/kinds" "github.com/spf13/afero" "github.com/spf13/cobra" ) -var ( - errHelp = errors.New("help requested") -) +var errHelp = errors.New("help requested") // Execute executes a command. func Execute(args []string) error { @@ -63,6 +66,12 @@ func Execute(args []string) error { } args = mapLegacyArgs(args) cd, err := x.Execute(context.Background(), args) + if cd != nil { + if closer, ok := cd.Root.Command.(types.Closer); ok { + closer.Close() + } + } + if err != nil { if err == errHelp { cd.CobraCommand.Help() @@ -85,11 +94,17 @@ type commonConfig struct { fs *hugofs.Fs } +type configKey struct { + counter int32 + ignoreModulesDoesNotExists bool +} + // This is the root command. type rootCommand struct { - Printf func(format string, v ...interface{}) - Println func(a ...interface{}) - Out io.Writer + Printf func(format string, v ...any) + Println func(a ...any) + StdOut io.Writer + StdErr io.Writer logger loggers.Logger @@ -98,8 +113,11 @@ type rootCommand struct { // Some, but not all commands need access to these. // Some needs more than one, so keep them in a small cache. - commonConfigs *lazycache.Cache[int32, *commonConfig] - hugoSites *lazycache.Cache[int32, *hugolib.HugoSites] + commonConfigs *lazycache.Cache[configKey, *commonConfig] + hugoSites *lazycache.Cache[configKey, *hugolib.HugoSites] + + // changesFromBuild received from Hugo in watch mode. + changesFromBuild chan []identity.Identity commands []simplecobra.Commander @@ -109,13 +127,10 @@ type rootCommand struct { environment string // Common build flags. - baseURL string - gc bool - poll string - panicOnWarning bool - forceSyncStatic bool - printPathWarnings bool - printUnusedTemplates bool + baseURL string + gc bool + poll string + forceSyncStatic bool // Profile flags (for debugging of performance problems) cpuprofile string @@ -124,17 +139,31 @@ type rootCommand struct { traceprofile string printm bool - // TODO(bep) var vs string - logging bool - verbose bool - verboseLog bool - debug bool - quiet bool + logLevel string + + quiet bool + devMode bool // Hidden flag. + renderToMemory bool cfgFile string cfgDir string - logFile string +} + +func (r *rootCommand) isVerbose() bool { + return r.logger.Level() <= logg.LevelInfo +} + +func (r *rootCommand) Close() error { + if r.hugoSites != nil { + r.hugoSites.DeleteFunc(func(key configKey, value *hugolib.HugoSites) bool { + if value != nil { + value.Close() + } + return false + }) + } + return nil } func (r *rootCommand) Build(cd *simplecobra.Commandeer, bcfg hugolib.BuildCfg, cfg config.Provider) (*hugolib.HugoSites, error) { @@ -153,17 +182,18 @@ func (r *rootCommand) Commands() []simplecobra.Commander { return r.commands } -func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commonConfig, error) { - cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) { +func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*commonConfig, error) { + cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) { fs := oldConf.fs configs, err := allconfig.LoadConfig( allconfig.ConfigSourceDescriptor{ - Flags: oldConf.cfg, - Fs: fs.Source, - Filename: r.cfgFile, - ConfigDir: r.cfgDir, - Logger: r.logger, - Environment: r.environment, + Flags: oldConf.cfg, + Fs: fs.Source, + Filename: r.cfgFile, + ConfigDir: r.cfgDir, + Logger: r.logger, + Environment: r.environment, + IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists, }, ) if err != nil { @@ -172,7 +202,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{ @@ -181,18 +211,16 @@ func (r *rootCommand) ConfigFromConfig(key int32, oldConf *commonConfig) (*commo cfg: oldConf.cfg, fs: fs, }, nil - }) return cc, err - } -func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commonConfig, error) { +func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*commonConfig, error) { if cfg == nil { panic("cfg must be set") } - cc, _, err := r.commonConfigs.GetOrCreate(key, func(key int32) (*commonConfig, error) { + cc, _, err := r.commonConfigs.GetOrCreate(key, func(key configKey) (*commonConfig, error) { var dir string if r.source != "" { dir, _ = filepath.Abs(r.source) @@ -204,13 +232,10 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo cfg = config.New() } - if !cfg.IsSet("renderToDisk") { - cfg.Set("renderToDisk", true) - } if !cfg.IsSet("workingDir") { cfg.Set("workingDir", dir) } else { - if err := os.MkdirAll(cfg.GetString("workingDir"), 0777); err != nil { + if err := os.MkdirAll(cfg.GetString("workingDir"), 0o777); err != nil { return nil, fmt.Errorf("failed to create workingDir: %w", err) } } @@ -218,12 +243,13 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo // Load the config first to allow publishDir to be configured in config file. configs, err := allconfig.LoadConfig( allconfig.ConfigSourceDescriptor{ - Flags: cfg, - Fs: hugofs.Os, - Filename: r.cfgFile, - ConfigDir: r.cfgDir, - Environment: r.environment, - Logger: r.logger, + Flags: cfg, + Fs: hugofs.Os, + Filename: r.cfgFile, + ConfigDir: r.cfgDir, + Environment: r.environment, + Logger: r.logger, + IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists, }, ) if err != nil { @@ -239,11 +265,9 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo renderStaticToDisk := cfg.GetBool("renderStaticToDisk") sourceFs := hugofs.Os - var desinationFs afero.Fs - if cfg.GetBool("renderToDisk") { - desinationFs = hugofs.Os - } else { - desinationFs = afero.NewMemMapFs() + var destinationFs afero.Fs + if cfg.GetBool("renderToMemory") { + destinationFs = afero.NewMemMapFs() if renderStaticToDisk { // Hybrid, render dynamic content to Root. cfg.Set("publishDirDynamic", "/") @@ -252,16 +276,18 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo cfg.Set("publishDirDynamic", "/") cfg.Set("publishDirStatic", "/") } + } else { + destinationFs = hugofs.Os } - fs := hugofs.NewFromSourceAndDestination(sourceFs, desinationFs, cfg) + fs := hugofs.NewFromSourceAndDestination(sourceFs, destinationFs, cfg) if renderStaticToDisk { dynamicFs := fs.PublishDir publishDirStatic := cfg.GetString("publishDirStatic") workingDir := cfg.GetString("workingDir") absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic) - staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic) + staticFs := hugofs.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic) // Serve from both the static and dynamic fs, // the first will take priority. @@ -282,10 +308,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) @@ -302,40 +328,49 @@ func (r *rootCommand) ConfigFromProvider(key int32, cfg config.Provider) (*commo }) return cc, err - } func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) { - h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) { - depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger} + k := configKey{counter: r.configVersionID.Load()} + h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) { + depsCfg := r.newDepsConfig(conf) return hugolib.NewHugoSites(depsCfg) }) return h, err } func (r *rootCommand) Hugo(cfg config.Provider) (*hugolib.HugoSites, error) { - h, _, err := r.hugoSites.GetOrCreate(r.configVersionID.Load(), func(key int32) (*hugolib.HugoSites, error) { + return r.getOrCreateHugo(cfg, false) +} + +func (r *rootCommand) getOrCreateHugo(cfg config.Provider, ignoreModuleDoesNotExist bool) (*hugolib.HugoSites, error) { + k := configKey{counter: r.configVersionID.Load(), ignoreModulesDoesNotExists: ignoreModuleDoesNotExist} + h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) { conf, err := r.ConfigFromProvider(key, cfg) if err != nil { return nil, err } - depsCfg := deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, Logger: r.logger} + depsCfg := r.newDepsConfig(conf) return hugolib.NewHugoSites(depsCfg) }) return h, err } +func (r *rootCommand) newDepsConfig(conf *commonConfig) deps.DepsCfg { + return deps.DepsCfg{Configs: conf.configs, Fs: conf.fs, StdOut: r.logger.StdOut(), StdErr: r.logger.StdErr(), LogLevel: r.logger.Level(), ChangesFromBuild: r.changesFromBuild} +} + func (r *rootCommand) Name() string { return "hugo" } func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - if !r.buildWatch { - defer r.timeTrack(time.Now(), "Total") - } - b := newHugoBuilder(r, nil) + if !r.buildWatch { + defer b.postBuild("Total", time.Now()) + } + if err := b.loadConfig(cd, false); err != nil { return err } @@ -345,9 +380,11 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args defer r.timeTrack(time.Now(), "Built") } err := b.build() - return err + if err != nil { + return err + } + return nil }() - if err != nil { return err } @@ -385,18 +422,23 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args } func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - r.Out = os.Stdout + r.StdOut = os.Stdout + r.StdErr = os.Stderr if r.quiet { - r.Out = io.Discard + r.StdOut = io.Discard + r.StdErr = io.Discard } - r.Printf = func(format string, v ...interface{}) { + // Used by mkcert (server). + log.SetOutput(r.StdOut) + + r.Printf = func(format string, v ...any) { if !r.quiet { - fmt.Fprintf(r.Out, format, v...) + fmt.Fprintf(r.StdOut, format, v...) } } - r.Println = func(a ...interface{}) { + r.Println = func(a ...any) { if !r.quiet { - fmt.Fprintln(r.Out, a...) + fmt.Fprintln(r.StdOut, a...) } } _, running := runner.Command.(*serverCommand) @@ -405,57 +447,60 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error { if err != nil { return err } + // Set up the global logger early to allow info deprecations during config load. + loggers.SetGlobalLogger(r.logger) - loggers.PanicOnWarning.Store(r.panicOnWarning) - r.commonConfigs = lazycache.New[int32, *commonConfig](lazycache.Options{MaxEntries: 5}) - r.hugoSites = lazycache.New[int32, *hugolib.HugoSites](lazycache.Options{MaxEntries: 5}) + r.changesFromBuild = make(chan []identity.Identity, 10) + + r.commonConfigs = lazycache.New(lazycache.Options[configKey, *commonConfig]{MaxEntries: 5}) + // We don't want to keep stale HugoSites in memory longer than needed. + r.hugoSites = lazycache.New(lazycache.Options[configKey, *hugolib.HugoSites]{ + MaxEntries: 1, + OnEvict: func(key configKey, value *hugolib.HugoSites) { + value.Close() + runtime.GC() + }, + }) return nil } func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) { - var ( - logHandle = io.Discard - logThreshold = jww.LevelWarn - outHandle = r.Out - stdoutThreshold = jww.LevelWarn - ) + level := logg.LevelWarn - if r.verboseLog || r.logging || (r.logFile != "") { - var err error - if r.logFile != "" { - logHandle, err = os.OpenFile(r.logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) - if err != nil { - return nil, fmt.Errorf("Failed to open log file %q: %s", r.logFile, err) - } - } else { - logHandle, err = os.CreateTemp("", "hugo") - if err != nil { - return nil, err + if r.devMode { + level = logg.LevelTrace + } else { + if r.logLevel != "" { + switch strings.ToLower(r.logLevel) { + case "debug": + level = logg.LevelDebug + case "info": + level = logg.LevelInfo + case "warn", "warning": + level = logg.LevelWarn + case "error": + level = logg.LevelError + default: + return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel) } } - } else if r.verbose { - stdoutThreshold = jww.LevelInfo } - if r.debug { - stdoutThreshold = jww.LevelDebug + optsLogger := loggers.Options{ + DistinctLevel: logg.LevelWarn, + Level: level, + StdOut: r.StdOut, + StdErr: r.StdErr, + StoreErrors: running, } - if r.verboseLog { - logThreshold = jww.LevelInfo - if r.debug { - logThreshold = jww.LevelDebug - } - } - - loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle) - helpers.InitLoggers() - return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil + return loggers.New(optsLogger), nil } -func (r *rootCommand) Reset() { +func (r *rootCommand) resetLogs() { r.logger.Reset() + loggers.Log().Reset() } // IsTestRun reports whether the command is running as a test. @@ -464,64 +509,71 @@ func (r *rootCommand) IsTestRun() bool { } func (r *rootCommand) Init(cd *simplecobra.Commandeer) error { + return r.initRootCommand("", cd) +} + +func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Use = "hugo [flags]" - cmd.Short = "hugo builds your site" - cmd.Long = `hugo is the main command, used to build your Hugo site. + commandName := "hugo" + if subCommandName != "" { + commandName = subCommandName + } + cmd.Use = fmt.Sprintf("%s [flags]", commandName) + cmd.Short = "Build your site" + cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site. Hugo is a Fast and Flexible Static Site Generator built with love by spf13 and friends in Go. Complete documentation is available at https://gohugo.io/.` + cmd.Long = strings.ReplaceAll(cmd.Long, "COMMAND_NAME", commandName) + // Configure persistent flags cmd.PersistentFlags().StringVarP(&r.source, "source", "s", "", "filesystem path to read files relative from") - cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.MarkFlagDirname("source") cmd.PersistentFlags().StringP("destination", "d", "", "filesystem path to write files to") - cmd.PersistentFlags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.MarkFlagDirname("destination") cmd.PersistentFlags().StringVarP(&r.environment, "environment", "e", "", "build environment") + _ = cmd.RegisterFlagCompletionFunc("environment", cobra.NoFileCompletions) cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory") + _ = cmd.MarkFlagDirname("themesDir") cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern") + cmd.PersistentFlags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file") + _ = cmd.RegisterFlagCompletionFunc("ignoreVendorPaths", cobra.NoFileCompletions) cmd.PersistentFlags().String("clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00") + _ = cmd.RegisterFlagCompletionFunc("clock", cobra.NoFileCompletions) cmd.PersistentFlags().StringVar(&r.cfgFile, "config", "", "config file (default is hugo.yaml|json|toml)") + _ = cmd.MarkFlagFilename("config", config.ValidConfigFileExtensions...) cmd.PersistentFlags().StringVar(&r.cfgDir, "configDir", "config", "config dir") + _ = cmd.MarkFlagDirname("configDir") cmd.PersistentFlags().BoolVar(&r.quiet, "quiet", false, "build in quiet mode") + cmd.PersistentFlags().BoolVarP(&r.renderToMemory, "renderToMemory", "M", false, "render to memory (mostly useful when running the server)") - // Set bash-completion - _ = cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions) - - cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output") - cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output") - cmd.PersistentFlags().BoolVar(&r.logging, "log", false, "enable Logging") - cmd.PersistentFlags().StringVar(&r.logFile, "logFile", "", "log File path (if set, logging enabled automatically)") - cmd.PersistentFlags().BoolVar(&r.verboseLog, "verboseLog", false, "verbose logging") + cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.") + cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)") + _ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp)) cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed") - cmd.Flags().BoolVar(&r.renderToMemory, "renderToMemory", false, "render to memory (only useful for benchmark testing)") - // Set bash-completion - _ = cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{}) + cmd.PersistentFlags().MarkHidden("devMode") // Configure local flags applyLocalFlagsBuild(cmd, r) - // Set bash-completion. - // Each flag must first be defined before using the SetAnnotation() call. - _ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{}) - return nil } // A sub set of the complete build flags. These flags are used by new and mod. func applyLocalFlagsBuildConfig(cmd *cobra.Command, r *rootCommand) { cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)") + _ = cmd.MarkFlagDirname("theme") cmd.Flags().StringVarP(&r.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/") - cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/") - _ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{}) + cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory") + _ = cmd.MarkFlagDirname("cacheDir") cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory") - _ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"}) - + cmd.Flags().StringSliceP("renderSegments", "", []string{}, "named segments to render (configured in the segments config)") } // Flags needed to do a build (used by hugo and hugo server commands) @@ -534,18 +586,19 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) { cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory") cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages") cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory") + _ = cmd.MarkFlagDirname("layoutDir") cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build") cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes") - cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log") + _ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions) + cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log") cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions") cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics") cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.") cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files") 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") @@ -558,9 +611,8 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) { cmd.Flags().MarkHidden("profile-mutex") cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)") + _ = cmd.RegisterFlagCompletionFunc("disableKinds", cobra.FixedCompletions(kinds.AllKinds, cobra.ShellCompDirectiveNoFileComp)) cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)") - _ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{}) - } func (r *rootCommand) timeTrack(start time.Time, name string) { diff --git a/commands/commands.go b/commands/commands.go index 9d707b841..10ab106e2 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -14,6 +14,8 @@ package commands import ( + "context" + "github.com/bep/simplecobra" ) @@ -21,6 +23,7 @@ import ( func newExec() (*simplecobra.Exec, error) { rootCmd := &rootCommand{ commands: []simplecobra.Commander{ + newHugoBuildCmd(), newVersionCmd(), newEnvCommand(), newServerCommand(), @@ -37,5 +40,34 @@ func newExec() (*simplecobra.Exec, error) { } return simplecobra.New(rootCmd) - +} + +func newHugoBuildCmd() simplecobra.Commander { + return &hugoBuildCommand{} +} + +// hugoBuildCommand just delegates to the rootCommand. +type hugoBuildCommand struct { + rootCmd *rootCommand +} + +func (c *hugoBuildCommand) Commands() []simplecobra.Commander { + return nil +} + +func (c *hugoBuildCommand) Name() string { + return "build" +} + +func (c *hugoBuildCommand) Init(cd *simplecobra.Commandeer) error { + c.rootCmd = cd.Root.Command.(*rootCommand) + return c.rootCmd.initRootCommand("build", cd) +} + +func (c *hugoBuildCommand) PreRun(cd, runner *simplecobra.Commandeer) error { + return c.rootCmd.PreRun(cd, runner) +} + +func (c *hugoBuildCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { + return c.rootCmd.Run(ctx, cd, args) } diff --git a/commands/config.go b/commands/config.go index 44c5a1c32..7d166b9b8 100644 --- a/commands/config.go +++ b/commands/config.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -23,9 +23,12 @@ import ( "time" "github.com/bep/simplecobra" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/spf13/cobra" ) // newConfigCommand creates a new config command and its subcommands. @@ -35,13 +38,14 @@ func newConfigCommand() *configCommand { &configMountsCommand{}, }, } - } type configCommand struct { r *rootCommand - format string + format string + lang string + printZero bool commands []simplecobra.Commander } @@ -55,18 +59,27 @@ func (c *configCommand) Name() string { } func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil)) + conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil)) if err != nil { return err } - config := conf.configs.Base + var config *allconfig.Config + if c.lang != "" { + var found bool + config, found = conf.configs.LanguageConfigMap[c.lang] + if !found { + return fmt.Errorf("language %q not found", c.lang) + } + } else { + config = conf.configs.LanguageConfigSlice[0] + } var buf bytes.Buffer dec := json.NewEncoder(&buf) dec.SetIndent("", " ") dec.SetEscapeHTML(false) - if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil { + if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: !c.printZero}); err != nil { return err } @@ -77,10 +90,11 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg os.Stdout.Write(buf.Bytes()) default: // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. - var m map[string]interface{} + var m map[string]any if err := json.Unmarshal(buf.Bytes(), &m); err != nil { return err } + maps.ConvertFloat64WithNoDecimalsToInt(m) switch format { case "yaml": return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout) @@ -97,9 +111,13 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg func (c *configCommand) Init(cd *simplecobra.Commandeer) error { c.r = cd.Root.Command.(*rootCommand) cmd := cd.CobraCommand - cmd.Short = "Print the site configuration" - cmd.Long = `Print the site configuration, both default and custom settings.` + cmd.Short = "Display site configuration" + cmd.Long = `Display site configuration, both default and custom settings.` cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)") + _ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) + cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.") + cmd.Flags().BoolVar(&c.printZero, "printZero", false, `include config options with zero values (e.g. false, 0, "") in the output`) + _ = cmd.RegisterFlagCompletionFunc("lang", cobra.NoFileCompletions) applyLocalFlagsBuildConfig(cmd, c.r) return nil @@ -176,7 +194,6 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) { Dir: m.m.Dir(), Mounts: mounts, }) - } type configMountsCommand struct { @@ -194,13 +211,13 @@ func (c *configMountsCommand) Name() string { func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { r := c.configCmd.r - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil)) + conf, err := r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, nil)) if err != nil { return err } for _, m := range conf.configs.Modules { - if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.verbose}, metadecoders.JSON, os.Stdout); err != nil { + if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.isVerbose()}, metadecoders.JSON, os.Stdout); err != nil { return err } } @@ -211,6 +228,7 @@ func (c *configMountsCommand) Init(cd *simplecobra.Commandeer) error { c.r = cd.Root.Command.(*rootCommand) cmd := cd.CobraCommand cmd.Short = "Print the configured file mounts" + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, c.r) return nil } diff --git a/commands/convert.go b/commands/convert.go index 765fb5f54..ebf81cfb3 100644 --- a/commands/convert.go +++ b/commands/convert.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -46,6 +46,7 @@ to use JSON for the front matter.`, return c.convertContents(metadecoders.JSON) }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions }, }, &simpleCommand{ @@ -57,6 +58,7 @@ to use TOML for the front matter.`, return c.convertContents(metadecoders.TOML) }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions }, }, &simpleCommand{ @@ -68,6 +70,7 @@ to use YAML for the front matter.`, return c.convertContents(metadecoders.YAML) }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions }, }, }, @@ -84,7 +87,7 @@ type convertCommand struct { r *rootCommand h *hugolib.HugoSites - // Commmands. + // Commands. commands []simplecobra.Commander } @@ -102,14 +105,16 @@ func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, ar func (c *convertCommand) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "Convert your content to different formats" - cmd.Long = `Convert your content (e.g. front matter) to different formats. + cmd.Short = "Convert front matter to another format" + cmd.Long = `Convert front matter to another format. 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.MarkFlagDirname("output") cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first") + cmd.RunE = nil return nil } @@ -133,14 +138,14 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar } } - if p.File().IsZero() { + if p.File() == nil { // No content file. return nil } errMsg := fmt.Errorf("error processing file %q", p.File().Path()) - site.Log.Infoln("ttempting to convert", p.File().Filename()) + site.Log.Infoln("attempting to convert", p.File().Filename()) f := p.File() file, err := f.FileInfo().Meta().Open() @@ -208,7 +213,7 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error { var pagesBackedByFile page.Pages for _, p := range site.AllPages() { - if p.File().IsZero() { + if p.File() == nil { continue } pagesBackedByFile = append(pagesBackedByFile, p) diff --git a/commands/deploy.go b/commands/deploy.go index 8dae4bd88..3e9d3df20 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,38 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy - -// 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. +//go:build withdeploy package commands import ( "context" - "github.com/bep/simplecobra" "github.com/gohugoio/hugo/deploy" + + "github.com/bep/simplecobra" "github.com/spf13/cobra" ) func newDeployCommand() simplecobra.Commander { - return &simpleCommand{ name: "deploy", - short: "Deploy your site to a Cloud provider.", - long: `Deploy your site to a Cloud provider. + short: "Deploy your site to a cloud provider", + long: `Deploy your site to a cloud provider See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed documentation. @@ -52,20 +38,14 @@ documentation. if err != nil { return err } - deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.PathSpec.PublishFs) + deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.Log, h.PathSpec.PublishFs) if err != nil { return err } return deployer.Deploy(ctx) }, withc: func(cmd *cobra.Command, r *rootCommand) { - cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one") - cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target") - cmd.Flags().Bool("dryRun", false, "dry run") - cmd.Flags().Bool("force", false, "force upload of all files") - cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target") - cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable") - cmd.Flags().Int("workers", 10, "number of workers to transfer files. defaults to 10") + applyDeployFlags(cmd, r) }, } } diff --git a/commands/deploy_flags.go b/commands/deploy_flags.go new file mode 100644 index 000000000..d4326547a --- /dev/null +++ b/commands/deploy_flags.go @@ -0,0 +1,33 @@ +// Copyright 2024 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 commands + +import ( + "github.com/gohugoio/hugo/deploy/deployconfig" + "github.com/spf13/cobra" +) + +func applyDeployFlags(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one") + _ = cmd.RegisterFlagCompletionFunc("target", cobra.NoFileCompletions) + cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target") + cmd.Flags().Bool("dryRun", false, "dry run") + cmd.Flags().Bool("force", false, "force upload of all files") + cmd.Flags().Bool("invalidateCDN", deployconfig.DefaultConfig.InvalidateCDN, "invalidate the CDN cache listed in the deployment target") + cmd.Flags().Int("maxDeletes", deployconfig.DefaultConfig.MaxDeletes, "maximum # of files to delete, or -1 to disable") + _ = cmd.RegisterFlagCompletionFunc("maxDeletes", cobra.NoFileCompletions) + cmd.Flags().Int("workers", deployconfig.DefaultConfig.Workers, "number of workers to transfer files. defaults to 10") + _ = cmd.RegisterFlagCompletionFunc("workers", cobra.NoFileCompletions) +} diff --git a/commands/deploy_off.go b/commands/deploy_off.go index b238d6cf7..8f5eaa2de 100644 --- a/commands/deploy_off.go +++ b/commands/deploy_off.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,10 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build nodeploy -// +build nodeploy +//go:build !withdeploy -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -30,8 +29,10 @@ package commands import ( + "context" "errors" + "github.com/bep/simplecobra" "github.com/spf13/cobra" ) @@ -39,9 +40,10 @@ func newDeployCommand() simplecobra.Commander { return &simpleCommand{ name: "deploy", run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - return nil + return errors.New("deploy not supported in this version of Hugo; install a release with 'withdeploy' in the archive filename or build yourself with the 'withdeploy' build tag. Also see https://github.com/gohugoio/hugo/pull/12995") }, withc: func(cmd *cobra.Command, r *rootCommand) { + applyDeployFlags(cmd, r) cmd.Hidden = true }, } diff --git a/commands/env.go b/commands/env.go index a6db551e9..753522560 100644 --- a/commands/env.go +++ b/commands/env.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -19,20 +19,21 @@ import ( "github.com/bep/simplecobra" "github.com/gohugoio/hugo/common/hugo" + "github.com/spf13/cobra" ) func newEnvCommand() simplecobra.Commander { return &simpleCommand{ name: "env", - short: "Print Hugo version and environment info", - long: "Print Hugo version and environment info. This is useful in Hugo bug reports", + short: "Display version and environment info", + long: "Display version and environment info. This is useful in Hugo bug reports", run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { r.Printf("%s\n", hugo.BuildVersionString()) r.Printf("GOOS=%q\n", runtime.GOOS) r.Printf("GOARCH=%q\n", runtime.GOARCH) r.Printf("GOVERSION=%q\n", runtime.Version()) - if r.verbose { + if r.isVerbose() { deps := hugo.GetDependencyList() for _, dep := range deps { r.Printf("%s\n", dep) @@ -47,6 +48,9 @@ func newEnvCommand() simplecobra.Commander { } return nil }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, } } @@ -57,7 +61,10 @@ func newVersionCmd() simplecobra.Commander { r.Println(hugo.BuildVersionString()) return nil }, - short: "Print Hugo version and environment info", - long: "Print Hugo version and environment info. This is useful in Hugo bug reports.", + short: "Display version", + long: "Display version and environment info. This is useful in Hugo bug reports.", + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, } } diff --git a/commands/gen.go b/commands/gen.go index c5eab894a..1c5361840 100644 --- a/commands/gen.go +++ b/commands/gen.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -14,12 +14,14 @@ package commands import ( + "bytes" "context" "encoding/json" "fmt" "os" "path" "path/filepath" + "slices" "strings" "github.com/alecthomas/chroma/v2" @@ -30,8 +32,11 @@ import ( "github.com/gohugoio/hugo/docshelper" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/parser" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + "gopkg.in/yaml.v2" ) func newGenCommand() *genCommand { @@ -41,9 +46,11 @@ func newGenCommand() *genCommand { genmandir string // Chroma flags. - style string - highlightStyle string - linesStyle string + style string + highlightStyle string + lineNumbersInlineStyle string + lineNumbersTableStyle string + omitEmpty bool ) newChromaStyles := func() simplecobra.Commander { @@ -55,25 +62,49 @@ func newGenCommand() *genCommand { See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { + style = strings.ToLower(style) + if !slices.Contains(styles.Names(), style) { + return fmt.Errorf("invalid style: %s", style) + } builder := styles.Get(style).Builder() if highlightStyle != "" { builder.Add(chroma.LineHighlight, highlightStyle) } - if linesStyle != "" { - builder.Add(chroma.LineNumbers, linesStyle) + if lineNumbersInlineStyle != "" { + builder.Add(chroma.LineNumbers, lineNumbersInlineStyle) + } + if lineNumbersTableStyle != "" { + builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle) } style, err := builder.Build() if err != nil { return err } - formatter := html.New(html.WithAllClasses(true)) - formatter.WriteCSS(os.Stdout, style) + + var formatter *html.Formatter + if omitEmpty { + formatter = html.New(html.WithClasses(true)) + } else { + formatter = html.New(html.WithAllClasses(true)) + } + + w := os.Stdout + fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " ")) + formatter.WriteCSS(w, style) return nil }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)") - cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)") - cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)") + _ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions) + cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`) + _ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions) + cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`) + _ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions) + cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`) + _ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions) + cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`) + _ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions) }, } } @@ -97,7 +128,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl } if found, _ := helpers.Exists(genmandir, hugofs.Os); !found { r.Println("Directory", genmandir, "does not exist, creating...") - if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil { + if err := hugofs.Os.MkdirAll(genmandir, 0o777); err != nil { return err } } @@ -111,9 +142,9 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl return nil }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.") - // For bash-completion - cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.MarkFlagDirname("dir") }, } } @@ -128,11 +159,11 @@ url: %s return &simpleCommand{ name: "doc", - short: "Generate Markdown documentation for the Hugo CLI.", + short: "Generate Markdown documentation for the Hugo CLI", long: `Generate Markdown documentation for the Hugo CLI. This command is, mostly, used to create up-to-date documentation of Hugo's command-line interface for https://gohugo.io/. - + It creates one Markdown file per command with front matter suitable for rendering in Hugo.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { @@ -146,20 +177,20 @@ url: %s } if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found { r.Println("Directory", gendocdir, "does not exist, creating...") - if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil { + if err := hugofs.Os.MkdirAll(gendocdir, 0o777); err != nil { return err } } prepender := func(filename string) string { name := filepath.Base(filename) base := strings.TrimSuffix(name, path.Ext(name)) - url := "/commands/" + strings.ToLower(base) + "/" + url := "/docs/reference/commands/" + strings.ToLower(base) + "/" return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url) } linkHandler := func(name string) string { base := strings.TrimSuffix(name, path.Ext(name)) - return "/commands/" + strings.ToLower(base) + "/" + return "/docs/reference/commands/" + strings.ToLower(base) + "/" } r.Println("Generating Hugo command-line documentation in", gendocdir, "...") doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler) @@ -168,12 +199,11 @@ url: %s return nil }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.") - // For bash-completion - cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{}) + _ = cmd.MarkFlagDirname("dir") }, } - } var docsHelperTarget string @@ -181,23 +211,41 @@ url: %s newDocsHelper := func() simplecobra.Commander { return &simpleCommand{ name: "docshelper", - short: "Generate some data files for the Hugo docs.", + short: "Generate some data files for the Hugo docs", run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { r.Println("Generate docs data to", docsHelperTarget) - targetFile := filepath.Join(docsHelperTarget, "docs.json") + var buf bytes.Buffer + jsonEnc := json.NewEncoder(&buf) + + configProvider := func() docshelper.DocProvider { + conf := hugolib.DefaultConfig() + conf.CacheDir = "" // The default value does not make sense in the docs. + defaultConfig := parser.NullBoolJSONMarshaller{Wrapped: parser.LowerCaseCamelJSONMarshaller{Value: conf}} + return docshelper.DocProvider{"config": defaultConfig} + } + + docshelper.AddDocProviderFunc(configProvider) + if err := jsonEnc.Encode(docshelper.GetDocProvider()); err != nil { + return err + } + + // Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format. + var m map[string]any + if err := json.Unmarshal(buf.Bytes(), &m); err != nil { + return err + } + + targetFile := filepath.Join(docsHelperTarget, "docs.yaml") f, err := os.Create(targetFile) if err != nil { return err } defer f.Close() - - enc := json.NewEncoder(f) - enc.SetIndent("", " ") - - if err := enc.Encode(docshelper.GetDocProvider()); err != nil { + yamlEnc := yaml.NewEncoder(f) + if err := yamlEnc.Encode(m); err != nil { return err } @@ -206,6 +254,7 @@ url: %s }, withc: func(cmd *cobra.Command, r *rootCommand) { cmd.Hidden = true + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.PersistentFlags().StringVarP(&docsHelperTarget, "dir", "", "docs/data", "data dir") }, } @@ -219,7 +268,6 @@ url: %s newDocsHelper(), }, } - } type genCommand struct { @@ -242,7 +290,10 @@ 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.Short = "Generate documentation and syntax highlighting styles" + cmd.Long = "Generate documentation for your project using Hugo's documentation engine, including syntax highlighting for various programming languages." + + cmd.RunE = nil return nil } diff --git a/commands/helpers.go b/commands/helpers.go index c342ce2c7..a13bdebc2 100644 --- a/commands/helpers.go +++ b/commands/helpers.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -14,7 +14,6 @@ package commands import ( - "bytes" "errors" "fmt" "log" @@ -24,8 +23,6 @@ import ( "github.com/bep/simplecobra" "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/helpers" - "github.com/spf13/afero" "github.com/spf13/pflag" ) @@ -79,22 +76,19 @@ 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. internalKeySet := map[string]bool{ - "quiet": true, - "verbose": true, - "watch": true, - "disableLiveReload": true, - "liveReloadPort": true, - "renderToMemory": true, - "clock": true, + "quiet": true, + "verbose": true, + "watch": true, + "liveReloadPort": true, + "renderToMemory": true, + "clock": true, } cmd := cd.CobraCommand @@ -116,20 +110,11 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P }) return cfg - } func mkdir(x ...string) { p := filepath.Join(x...) - err := os.MkdirAll(p, 0777) // before umask - if err != nil { - log.Fatal(err) - } -} - -func touchFile(fs afero.Fs, filename string) { - mkdir(filepath.Dir(filename)) - err := helpers.WriteToDisk(filename, bytes.NewReader([]byte{}), fs) + err := os.MkdirAll(p, 0o777) // before umask if err != nil { log.Fatal(err) } diff --git a/commands/hugo_windows.go b/commands/hugo_windows.go index e1fd98132..c354e889d 100644 --- a/commands/hugo_windows.go +++ b/commands/hugo_windows.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -25,9 +25,9 @@ func init() { // This message to show to Windows users if Hugo is opened from explorer.exe cobra.MousetrapHelpText = ` - Hugo is a command-line tool for generating static website. + Hugo is a command-line tool for generating static websites. + + You need to open PowerShell and run Hugo from there. - You need to open cmd.exe and run Hugo from there. - Visit https://gohugo.io/ for more information.` } diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go index 95dbb1ca8..3b57ac5e9 100644 --- a/commands/hugobuilder.go +++ b/commands/hugobuilder.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -24,6 +24,7 @@ import ( "runtime/trace" "strings" "sync" + "sync/atomic" "time" "github.com/bep/simplecobra" @@ -31,7 +32,9 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/htime" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/terminal" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" @@ -39,11 +42,10 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/gohugoio/hugo/identity" "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" @@ -60,7 +62,7 @@ type hugoBuilder struct { // Currently only set when in "fast render mode". changeDetector *fileChangeDetector - visitedURLs *types.EvictingStringQueue + visitedURLs *types.EvictingQueue[string] fullRebuildSem *semaphore.Weighted debounce func(f func()) @@ -68,15 +70,19 @@ type hugoBuilder struct { onConfigLoaded func(reloaded bool) error fastRenderMode bool - buildWatch bool showErrorInBrowser bool errState hugoBuilderErrState } +var errConfigNotSet = errors.New("config not set") + func (c *hugoBuilder) withConfE(fn func(conf *commonConfig) error) error { c.confmu.Lock() defer c.confmu.Unlock() + if c.conf == nil { + return errConfigNotSet + } return fn(c.conf) } @@ -84,7 +90,6 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) { c.confmu.Lock() defer c.confmu.Unlock() fn(c.conf) - } type hugoBuilderErrState struct { @@ -130,52 +135,14 @@ func (e *hugoBuilderErrState) wasErr() bool { return e.waserr } -func (c *hugoBuilder) errCount() int { - return int(c.r.logger.LogCounters().ErrorCounter.Count()) -} - // getDirList provides NewWatcher() with a list of directories to watch for changes. func (c *hugoBuilder) getDirList() ([]string, error) { - var filenames []string - - walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil { - c.r.logger.Errorln("walker: ", err) - return nil - } - - if fi.IsDir() { - if fi.Name() == ".git" || - fi.Name() == "node_modules" || fi.Name() == "bower_components" { - return filepath.SkipDir - } - - filenames = append(filenames, fi.Meta().Filename) - } - - return nil - } - h, err := c.hugo() if err != nil { return nil, err } - watchFiles := h.PathSpec.BaseFs.WatchDirs() - for _, fi := range watchFiles { - if !fi.IsDir() { - filenames = append(filenames, fi.Meta().Filename) - continue - } - w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn}) - if err := w.Walk(); err != nil { - c.r.logger.Errorln("walker: ", err) - } - } - - filenames = helpers.UniqueStringsSorted(filenames) - - return filenames, nil + return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil } func (c *hugoBuilder) initCPUProfile() (func(), error) { @@ -363,7 +330,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa configFiles = conf.configs.LoadingInfo.ConfigFiles }) - c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", ")) + c.r.Println("Watching for config changes in", strings.Join(configFiles, ", ")) for _, configFile := range configFiles { watcher.Add(configFile) configSet[configFile] = true @@ -372,6 +339,26 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa go func() { for { select { + case changes := <-c.r.changesFromBuild: + unlock, err := h.LockBuild() + if err != nil { + c.r.logger.Errorln("Failed to acquire a build lock: %s", err) + return + } + c.changeDetector.PrepareNew() + err = c.rebuildSitesForChanges(changes) + if err != nil { + c.r.logger.Errorln("Error while watching:", err) + } + if c.s != nil && c.s.doLiveReload { + doReload := c.changeDetector == nil || len(c.changeDetector.changed()) > 0 + doReload = doReload || c.showErrorInBrowser && c.errState.buildErr() != nil + if doReload { + livereload.ForceRefresh() + } + } + unlock() + case evs := <-watcher.Events: unlock, err := h.LockBuild() if err != nil { @@ -379,7 +366,7 @@ func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*wa return } c.handleEvents(watcher, staticSyncer, evs, configSet) - if c.showErrorInBrowser && c.errCount() > 0 { + if c.showErrorInBrowser && c.errState.buildErr() != nil { // Need to reload browser to show the error livereload.ForceRefresh() } @@ -418,25 +405,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() } @@ -445,11 +413,17 @@ func (c *hugoBuilder) build() error { } func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) { - h, err := c.hugo() + defer func() { + c.errState.setBuildErr(err) + }() + + var h *hugolib.HugoSites + h, err = c.hugo() if err != nil { - return err + return } - return h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock}) + err = h.Build(hugolib.BuildCfg{NoBuildLock: noBuildLock}) + return } func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { @@ -461,6 +435,7 @@ func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { } func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { + infol := c.r.logger.InfoCommand("static") publishDir := helpers.FilePathSeparator if sourceFs.PublishFolder != "" { @@ -484,13 +459,13 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint syncer.SrcFs = fs if syncer.Delete { - c.r.logger.Infoln("removing all files from destination that don't exist in static dirs") + infol.Logf("removing all files from destination that don't exist in static dirs") - syncer.DeleteFilter = func(f os.FileInfo) bool { + syncer.DeleteFilter = func(f fsync.FileInfo) bool { return f.IsDir() && strings.HasPrefix(f.Name(), ".") } } - c.r.logger.Infoln("syncing static files to", publishDir) + start := time.Now() // because we are using a baseFs (to get the union right). // set sync src to root @@ -498,9 +473,10 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint if err != nil { return 0, err } + loggers.TimeTrackf(infol, start, nil, "syncing static files to %s", publishDir) - // Sync runs Stat 3 times for every source file (which sounds much) - numFiles := fs.statCounter / 3 + // Sync runs Stat 2 times for every source file. + numFiles := fs.statCounter / 2 return numFiles, err } @@ -545,14 +521,13 @@ func (c *hugoBuilder) fullBuild(noBuildLock bool) error { langCount map[string]uint64 ) - if !c.r.quiet { - fmt.Println("Start building sites … ") - fmt.Println(hugo.BuildVersionString()) - if terminal.IsTerminal(os.Stdout) { - defer func() { - fmt.Print(showCursor + clearLine) - }() - } + c.r.logger.Println("Start building sites … ") + c.r.logger.Println(hugo.BuildVersionString()) + c.r.logger.Println() + if terminal.IsTerminal(os.Stdout) { + defer func() { + fmt.Print(showCursor + clearLine) + }() } copyStaticFunc := func() error { @@ -637,13 +612,16 @@ func (c *hugoBuilder) fullRebuild(changeType string) { time.Sleep(2 * time.Second) }() - defer c.r.timeTrack(time.Now(), "Rebuilt") + defer c.postBuild("Rebuilt", time.Now()) err := c.reloadConfig() if err != nil { // Set the processing on pause until the state is recovered. c.errState.setPaused(true) c.handleBuildErr(err, "Failed to reload config") + if c.s.doLiveReload { + livereload.ForceRefresh() + } } else { c.errState.setPaused(false) } @@ -672,13 +650,44 @@ func (c *hugoBuilder) handleBuildErr(err error, msg string) { func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, staticSyncer *staticSyncer, evs []fsnotify.Event, - configSet map[string]bool) { + configSet map[string]bool, +) { defer func() { c.errState.setWasErr(false) }() var isHandled bool + // Filter out ghost events (from deleted, renamed directories). + // This seems to be a bug in fsnotify, or possibly MacOS. + var n int + for _, ev := range evs { + keep := true + // Write and rename operations are often followed by CHMOD. + // There may be valid use cases for rebuilding the site on CHMOD, + // but that will require more complex logic than this simple conditional. + // On OS X this seems to be related to Spotlight, see: + // https://github.com/go-fsnotify/fsnotify/issues/15 + // A workaround is to put your site(s) on the Spotlight exception list, + // but that may be a little mysterious for most end users. + // So, for now, we skip reload on CHMOD. + // We do have to check for WRITE though. On slower laptops a Chmod + // could be aggregated with other important events, and we still want + // to rebuild on those + if ev.Op == fsnotify.Chmod { + keep = false + } else if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) { + if _, err := os.Stat(ev.Name); err != nil { + keep = false + } + } + if keep { + evs[n] = ev + n++ + } + } + evs = evs[:n] + for _, ev := range evs { isConfig := configSet[ev.Name] configChangeType := configChangeConfig @@ -746,48 +755,39 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, return } - c.r.logger.Infoln("Received System Events:", evs) + c.r.logger.Debugln("Received System Events:", evs) staticEvents := []fsnotify.Event{} dynamicEvents := []fsnotify.Event{} - filtered := []fsnotify.Event{} + filterDuplicateEvents := func(evs []fsnotify.Event) []fsnotify.Event { + seen := make(map[string]bool) + var n int + for _, ev := range evs { + if seen[ev.Name] { + continue + } + seen[ev.Name] = true + evs[n] = ev + n++ + } + return evs[:n] + } + h, err := c.hugo() if err != nil { c.r.logger.Errorln("Error getting the Hugo object:", err) return } + n = 0 for _, ev := range evs { if h.ShouldSkipFileChangeEvent(ev) { continue } - // Check the most specific first, i.e. files. - contentMapped := h.ContentChanges.GetSymbolicLinkMappings(ev.Name) - if len(contentMapped) > 0 { - for _, mapped := range contentMapped { - filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op}) - } - continue - } - - // Check for any symbolic directory mapping. - - dir, name := filepath.Split(ev.Name) - - contentMapped = h.ContentChanges.GetSymbolicLinkMappings(dir) - - if len(contentMapped) == 0 { - filtered = append(filtered, ev) - continue - } - - for _, mapped := range contentMapped { - mappedFilename := filepath.Join(mapped, name) - filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op}) - } + evs[n] = ev + n++ } - - evs = filtered + evs = evs[:n] for _, ev := range evs { ext := filepath.Ext(ev.Name) @@ -795,6 +795,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, istemp := strings.HasSuffix(ext, "~") || (ext == ".swp") || // vim (ext == ".swx") || // vim + (ext == ".bck") || // helix (ext == ".tmp") || // generic temp file (ext == ".DS_Store") || // OSX Thumbnail baseName == "4913" || // vim @@ -808,6 +809,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, if istemp { continue } + if h.Deps.SourceSpec.IgnoreFile(ev.Name) { continue } @@ -816,22 +818,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, continue } - // Write and rename operations are often followed by CHMOD. - // There may be valid use cases for rebuilding the site on CHMOD, - // but that will require more complex logic than this simple conditional. - // On OS X this seems to be related to Spotlight, see: - // https://github.com/go-fsnotify/fsnotify/issues/15 - // A workaround is to put your site(s) on the Spotlight exception list, - // but that may be a little mysterious for most end users. - // So, for now, we skip reload on CHMOD. - // We do have to check for WRITE though. On slower laptops a Chmod - // could be aggregated with other important events, and we still want - // to rebuild on those - if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod { - continue - } - - walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error { + walkAdder := func(path string, f hugofs.FileMetaInfo) error { if f.IsDir() { c.r.logger.Println("adding created directory to watchlist", path) if err := watcher.Add(path); err != nil { @@ -847,11 +834,10 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, } // recursively add new directories to watch list - // When mkdir -p is used, only the top directory triggers an event (at least on OSX) - if ev.Op&fsnotify.Create == fsnotify.Create { + if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Rename) { c.withConf(func(conf *commonConfig) { if s, err := conf.fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { - _ = helpers.SymbolicWalk(conf.fs.Source, ev.Name, walkAdder) + _ = helpers.Walk(conf.fs.Source, ev.Name, walkAdder) } }) } @@ -863,6 +849,11 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, } } + lrl := c.r.logger.InfoCommand("livereload") + + staticEvents = filterDuplicateEvents(staticEvents) + dynamicEvents = filterDuplicateEvents(dynamicEvents) + if len(staticEvents) > 0 { c.printChangeDetected("Static files") @@ -883,19 +874,20 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, if c.s != nil && c.s.doLiveReload { // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized - // force refresh when more than one file if !c.errState.wasErr() && len(staticEvents) == 1 { - ev := staticEvents[0] h, err := c.hugo() if err != nil { c.r.logger.Errorln("Error getting the Hugo object:", err) return } - path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name) - path = h.RelURL(helpers.ToSlashTrimLeading(path), false) + path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(staticEvents[0].Name) + path = h.RelURL(paths.ToSlashTrimLeading(path), false) + + lrl.Logf("refreshing static file %q", path) livereload.RefreshPath(path) } else { + lrl.Logf("got %d static file change events, force refresh", len(staticEvents)) livereload.ForceRefresh() } } @@ -906,33 +898,47 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, h.BaseFs.SourceFilesystems, dynamicEvents) - onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents) + onePageName := pickOneWriteOrCreatePath(h.Conf.ContentTypes(), partitionedEvents.ContentEvents) c.printChangeDetected("") c.changeDetector.PrepareNew() func() { - defer c.r.timeTrack(time.Now(), "Total") + defer c.postBuild("Total", time.Now()) if err := c.rebuildSites(dynamicEvents); err != nil { c.handleBuildErr(err, "Rebuild failed") } }() if c.s != nil && c.s.doLiveReload { - if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 { - if c.errState.wasErr() { - livereload.ForceRefresh() - return + if c.errState.wasErr() { + livereload.ForceRefresh() + return + } + + changed := c.changeDetector.changed() + if c.changeDetector != nil { + if len(changed) >= 10 { + lrl.Logf("build changed %d files", len(changed)) + } else { + lrl.Logf("build changed %d files: %q", len(changed), changed) } - changed := c.changeDetector.changed() - if c.changeDetector != nil && len(changed) == 0 { + if len(changed) == 0 { // Nothing has changed. return - } else if len(changed) == 1 { - pathToRefresh := h.PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false) - livereload.RefreshPath(pathToRefresh) + } + } + + // If this change set also contains one or more CSS files, we need to + // refresh these as well. + var cssChanges []string + var otherChanges []string + + for _, ev := range changed { + if strings.HasSuffix(ev, ".css") { + cssChanges = append(cssChanges, ev) } else { - livereload.ForceRefresh() + otherChanges = append(otherChanges, ev) } } @@ -948,23 +954,57 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, } } - if p != nil { - livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort()) + if p != nil && p.RelPermalink() != "" { + link, port := p.RelPermalink(), p.Site().ServerPort() + lrl.Logf("navigating to %q using port %d", link, port) + livereload.NavigateToPathForPort(link, port) } else { + lrl.Logf("no page to navigate to, force refresh") livereload.ForceRefresh() } + } else if len(otherChanges) > 0 { + if len(otherChanges) == 1 { + // Allow single changes to be refreshed without a full page reload. + pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false) + lrl.Logf("refreshing %q", pathToRefresh) + livereload.RefreshPath(pathToRefresh) + } else if len(cssChanges) == 0 || len(otherChanges) > 1 { + lrl.Logf("force refresh") + livereload.ForceRefresh() + } + } else { + lrl.Logf("force refresh") + livereload.ForceRefresh() + } + + if len(cssChanges) > 0 { + // Allow some time for the live reload script to get reconnected. + if len(otherChanges) > 0 { + time.Sleep(200 * time.Millisecond) + } + for _, ev := range cssChanges { + pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(ev), false) + lrl.Logf("refreshing CSS %q", pathToRefresh) + livereload.RefreshPath(pathToRefresh) + } } } } } +func (c *hugoBuilder) postBuild(what string, start time.Time) { + if h, err := c.hugo(); err == nil && h.Conf.Running() { + h.LogServerAddresses() + } + c.r.timeTrack(start, what) +} + func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) { var h *hugolib.HugoSites if err := c.withConfE(func(conf *commonConfig) error { var err error h, err = c.r.HugFromConfig(conf) return err - }); err != nil { return nil, err } @@ -988,7 +1028,7 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites { func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error { cfg := config.New() - cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk)) + cfg.Set("renderToMemory", c.r.renderToMemory) watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch) if c.r.environment == "" { // We need to set the environment as early as possible because we need it to load the correct config. @@ -1009,17 +1049,19 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error cfg.Set("environment", c.r.environment) cfg.Set("internal", maps.Params{ - "running": running, - "watch": watch, - "verbose": c.r.verbose, + "running": running, + "watch": watch, + "verbose": c.r.isVerbose(), + "fastRenderMode": c.fastRenderMode, }) - conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg)) + conf, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, flagsToCfg(cd, cfg)) if err != nil { return err } if len(conf.configs.LoadingInfo.ConfigFiles) == 0 { + //lint:ignore ST1005 end user message. return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.") } @@ -1031,57 +1073,65 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error } return nil - } +var rebuildCounter atomic.Uint64 + func (c *hugoBuilder) printChangeDetected(typ string) { msg := "\nChange" if typ != "" { msg += " of " + typ } - msg += " detected, rebuilding site." + msg += fmt.Sprintf(" detected, rebuilding site (#%d).", rebuildCounter.Add(1)) c.r.logger.Println(msg) const layout = "2006-01-02 15:04:05.000 -0700" c.r.logger.Println(htime.Now().Format(layout)) } -func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error { +func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) (err error) { + defer func() { + c.errState.setBuildErr(err) + }() if err := c.errState.buildErr(); err != nil { ferrs := herrors.UnwrapFileErrorsWithErrorContext(err) for _, err := range ferrs { events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write}) } } - c.errState.setBuildErr(nil) - visited := c.visitedURLs.PeekAllSet() - h, err := c.hugo() + var h *hugolib.HugoSites + h, err = c.hugo() if err != nil { - return err + return } - 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) - if langPath != "" { - langPath = langPath + "/" - } - home := h.PrependBasePath("/"+langPath, false) - visited[home] = true - } - }) + err = h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}, events...) + return +} + +func (c *hugoBuilder) rebuildSitesForChanges(ids []identity.Identity) (err error) { + defer func() { + c.errState.setBuildErr(err) + }() + + var h *hugolib.HugoSites + h, err = c.hugo() + if err != nil { + return } - return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...) + whatChanged := &hugolib.WhatChanged{} + whatChanged.Add(ids...) + err = h.Build(hugolib.BuildCfg{NoBuildLock: true, WhatChanged: whatChanged, RecentlyTouched: c.visitedURLs, ErrRecovery: c.errState.wasErr()}) + + return } func (c *hugoBuilder) reloadConfig() error { - c.r.Reset() + c.r.resetLogs() c.r.configVersionID.Add(1) if err := c.withConfE(func(conf *commonConfig) error { oldConf := conf - newConf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), conf) + newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf) if err != nil { return err } diff --git a/commands/import.go b/commands/import.go index 30ada15f8..37a6b0dbf 100644 --- a/commands/import.go +++ b/commands/import.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -19,12 +19,10 @@ import ( "errors" "fmt" "io" + "log" "os" "path/filepath" "regexp" - - jww "github.com/spf13/jwalterweatherman" - "strconv" "strings" "time" @@ -60,6 +58,7 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root return c.importFromJekyll(args) }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory") }, }, @@ -67,7 +66,6 @@ Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root } return c - } type importCommand struct { @@ -92,11 +90,12 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg func (c *importCommand) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "Import your site from others." - cmd.Long = `Import your site from other web site generators like Jekyll. + cmd.Short = "Import a site from another system" + cmd.Long = `Import a site from another system. Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`." + cmd.RunE = nil return nil } @@ -299,7 +298,7 @@ func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate t } func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error { - jww.TRACE.Println("Converting", path) + log.Println("Converting", path) filename := filepath.Base(path) postDate, postName, err := c.parseJekyllFilename(filename) @@ -308,11 +307,11 @@ func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft return nil } - jww.TRACE.Println(filename, postDate, postName) + log.Println(filename, postDate, postName) targetFile := filepath.Join(targetDir, relPath) targetParentDir := filepath.Dir(targetFile) - os.MkdirAll(targetParentDir, 0777) + os.MkdirAll(targetParentDir, 0o777) contentBytes, err := os.ReadFile(path) if err != nil { @@ -367,7 +366,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl if _, ok := jekyllPostDirs[entry.Name()]; !ok { err = hugio.CopyDir(fs, sfp, dfp, nil) if err != nil { - jww.ERROR.Println(err) + c.r.logger.Errorln(err) } } } @@ -388,7 +387,7 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl if !isExcept && entry.Name()[0] != '.' && entry.Name()[0] != '_' { err = hugio.CopyFile(fs, sfp, dfp) if err != nil { - jww.ERROR.Println(err) + c.r.logger.Errorln(err) } } } @@ -398,7 +397,6 @@ func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyl } func (c *importCommand) importFromJekyll(args []string) error { - jekyllRoot, err := filepath.Abs(filepath.Clean(args[0])) if err != nil { return newUserError("path error:", args[0]) @@ -429,11 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error { c.r.Println("Importing...") fileCount := 0 - callback := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil { - return err - } - + callback := func(path string, fi hugofs.FileMetaInfo) error { if fi.IsDir() { return nil } @@ -462,16 +456,19 @@ func (c *importCommand) importFromJekyll(args []string) error { for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs { if hasAnyPostInDir { - if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil { + if err = helpers.Walk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil { return err } } } c.r.Println("Congratulations!", fileCount, "post(s) imported!") - c.r.Println("Now, start Hugo by yourself:\n" + - "$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove") - c.r.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove") + c.r.Println("Now, start Hugo by yourself:\n") + c.r.Println("cd " + args[1]) + c.r.Println("git init") + c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke") + c.r.Println("echo \"theme = 'ananke'\" > hugo.toml") + c.r.Println("hugo server") return nil } diff --git a/commands/list.go b/commands/list.go index 6458df875..42f3408ba 100644 --- a/commands/list.go +++ b/commands/list.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -23,15 +23,14 @@ import ( "time" "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" + "github.com/spf13/cobra" ) // newListCommand creates a new list command and its subcommands. func newListCommand() *listCommand { - createRecord := func(workingDir string, p page.Page) []string { return []string{ filepath.ToSlash(strings.TrimPrefix(p.File().Filename(), workingDir+string(os.PathSeparator))), @@ -42,12 +41,14 @@ func newListCommand() *listCommand { p.PublishDate().Format(time.RFC3339), strconv.FormatBool(p.Draft()), p.Permalink(), + p.Kind(), + p.Section(), } } list := func(cd *simplecobra.Commandeer, r *rootCommand, shouldInclude func(page.Page) bool, opts ...any) error { bcfg := hugolib.BuildCfg{SkipRender: true} - cfg := config.New() + cfg := flagsToCfg(cd, nil) for i := 0; i < len(opts); i += 2 { cfg.Set(opts[i].(string), opts[i+1]) } @@ -56,7 +57,7 @@ func newListCommand() *listCommand { return err } - writer := csv.NewWriter(r.Out) + writer := csv.NewWriter(r.StdOut) defer writer.Flush() writer.Write([]string{ @@ -68,6 +69,8 @@ func newListCommand() *listCommand { "publishDate", "draft", "permalink", + "kind", + "section", }) for _, p := range h.Pages() { @@ -76,29 +79,24 @@ func newListCommand() *listCommand { if err := writer.Write(record); err != nil { return err } - if err != nil { - return err - } } } return nil - } return &listCommand{ commands: []simplecobra.Commander{ &simpleCommand{ name: "drafts", - short: "List all drafts", - long: `List all of the drafts in your content directory.`, + short: "List draft content", + long: `List draft content.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { shouldInclude := func(p page.Page) bool { - if !p.Draft() || p.File().IsZero() { + if !p.Draft() || p.File() == nil { return false } return true - } return list(cd, r, shouldInclude, "buildDrafts", true, @@ -106,32 +104,37 @@ func newListCommand() *listCommand { "buildExpired", true, ) }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, }, &simpleCommand{ name: "future", - short: "List all posts dated in the future", - long: `List all of the posts in your content directory which will be posted in the future.`, + short: "List future content", + long: `List content with a future publication date.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { shouldInclude := func(p page.Page) bool { - if !resource.IsFuture(p) || p.File().IsZero() { + if !resource.IsFuture(p) || p.File() == nil { return false } return true - } return list(cd, r, shouldInclude, "buildFuture", true, "buildDrafts", true, ) }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, }, &simpleCommand{ name: "expired", - short: "List all posts already expired", - long: `List all of the posts in your content directory which has already expired.`, + short: "List expired content", + long: `List content with a past expiration date.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { shouldInclude := func(p page.Page) bool { - if !resource.IsExpired(p) || p.File().IsZero() { + if !resource.IsExpired(p) || p.File() == nil { return false } return true @@ -141,21 +144,40 @@ func newListCommand() *listCommand { "buildDrafts", true, ) }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, }, &simpleCommand{ name: "all", - short: "List all posts", - long: `List all of the posts in your content directory, include drafts, future and expired pages.`, + short: "List all content", + long: `List all content including draft, future, and expired.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { shouldInclude := func(p page.Page) bool { - return !p.File().IsZero() + return p.File() != nil } return list(cd, r, shouldInclude, "buildDrafts", true, "buildFuture", true, "buildExpired", true) }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, + }, + &simpleCommand{ + name: "published", + short: "List published content", + long: `List content that is not draft, future, or expired.`, + run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { + shouldInclude := func(p page.Page) bool { + return !p.Draft() && !resource.IsFuture(p) && !resource.IsExpired(p) && p.File() != nil + } + return list(cd, r, shouldInclude) + }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + }, }, }, } - } type listCommand struct { @@ -177,11 +199,12 @@ func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args func (c *listCommand) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "Listing out various types of content" - cmd.Long = `Listing out various types of content. + cmd.Short = "List content" + cmd.Long = `List content. List requires a subcommand, e.g. hugo list drafts` + cmd.RunE = nil return nil } diff --git a/commands/mod.go b/commands/mod.go index 36d4a5596..58155f9be 100644 --- a/commands/mod.go +++ b/commands/mod.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -44,16 +44,16 @@ func newModCommands() *modCommands { npmCommand := &simpleCommand{ name: "npm", - short: "Various npm helpers.", + short: "Various npm helpers", long: `Various npm (Node package manager) helpers.`, commands: []simplecobra.Commander{ &simpleCommand{ name: "pack", - short: "Experimental: Prepares and writes a composite package.json file for your project.", + short: "Experimental: Prepares and writes a composite package.json file for your project", long: `Prepares and writes a composite package.json file for your project. On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file -with the base dependency set. +with the base dependency set. This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project. @@ -62,6 +62,7 @@ removed from Hugo, but we need to test this out in "real life" to get a feel of so this may/will change in future versions of Hugo. `, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { @@ -69,7 +70,7 @@ so this may/will change in future versions of Hugo. if err != nil { return err } - return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs) + return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs) }, }, }, @@ -79,20 +80,21 @@ so this may/will change in future versions of Hugo. commands: []simplecobra.Commander{ &simpleCommand{ name: "init", - short: "Initialize this project as a Hugo Module.", + short: "Initialize this project as a Hugo Module", long: `Initialize this project as a Hugo Module. It will try to guess the module path, but you may help by passing it as an argument, e.g: - + hugo mod init github.com/gohugoio/testshortcodes - + Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module inside a subfolder on GitHub, as one example. `, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - h, err := r.Hugo(flagsToCfg(cd, nil)) + h, err := r.getOrCreateHugo(flagsToCfg(cd, nil), true) if err != nil { return err } @@ -100,19 +102,24 @@ so this may/will change in future versions of Hugo. if len(args) >= 1 { initPath = args[0] } - return h.Configs.ModulesClient.Init(initPath) + c := h.Configs.ModulesClient + if err := c.Init(initPath); err != nil { + return err + } + return nil }, }, &simpleCommand{ name: "verify", - short: "Verify dependencies.", + short: "Verify dependencies", long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification") }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil)) + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) if err != nil { return err } @@ -122,16 +129,17 @@ so this may/will change in future versions of Hugo. }, &simpleCommand{ name: "graph", - short: "Print a module dependency graph.", + short: "Print a module dependency graph", long: `Print a module dependency graph with information about module status (disabled, vendored). Note that for vendored modules, that is the version listed and not the one from go.mod. `, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification") }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil)) + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) if err != nil { return err } @@ -141,11 +149,13 @@ Note that for vendored modules, that is the version listed and not the one from }, &simpleCommand{ name: "clean", - short: "Delete the Hugo Module cache for the current project.", + short: "Delete the Hugo Module cache for the current project", long: `Delete the Hugo Module cache for the current project.`, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`) + _ = cmd.RegisterFlagCompletionFunc("pattern", cobra.NoFileCompletions) cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache") }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { @@ -165,8 +175,9 @@ Note that for vendored modules, that is the version listed and not the one from }, &simpleCommand{ name: "tidy", - short: "Remove unused entries in go.mod and go.sum.", + short: "Remove unused entries in go.mod and go.sum", withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { @@ -179,11 +190,12 @@ Note that for vendored modules, that is the version listed and not the one from }, &simpleCommand{ name: "vendor", - short: "Vendor all module dependencies into the _vendor directory.", + short: "Vendor all module dependencies into the _vendor directory", long: `Vendor all module dependencies into the _vendor directory. If a module is vendored, that is where Hugo will look for it's dependencies. `, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions applyLocalFlagsBuildConfig(cmd, r) }, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { @@ -197,21 +209,26 @@ Note that for vendored modules, that is the version listed and not the one from &simpleCommand{ name: "get", - short: "Resolves dependencies in your current Hugo Project.", + short: "Resolves dependencies in your current Hugo project", long: ` -Resolves dependencies in your current Hugo Project. +Resolves dependencies in your current Hugo project. Some examples: Install the latest version possible for a given module: hugo mod get github.com/gohugoio/testshortcodes - + Install a specific version: hugo mod get github.com/gohugoio/testshortcodes@v0.3.0 -Install the latest versions of all module dependencies: +Install the latest versions of all direct module dependencies: + + hugo mod get + hugo mod get ./... (recursive) + +Install the latest versions of all module dependencies (direct and indirect): hugo mod get -u hugo mod get -u ./... (recursive) @@ -220,11 +237,12 @@ Run "go help get" for more information. All flags available for "go get" is also ` + commonUsageMod, withc: func(cmd *cobra.Command, r *rootCommand) { cmd.DisableFlagParsing = true + cmd.ValidArgsFunction = cobra.NoFileCompletions }, 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 } @@ -254,13 +272,14 @@ Run "go help get" for more information. All flags available for "go get" is also if info.Name() == "go.mod" { // Found a module. dir := filepath.Dir(path) - r.Println("Update module in", dir) + cfg := config.New() cfg.Set("workingDir", dir) - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg)) + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Add(1)}, flagsToCfg(cd, cfg)) if err != nil { return err } + r.Println("Update module in", conf.configs.Base.WorkingDir) client := conf.configs.ModulesClient return client.Get(args...) @@ -269,7 +288,7 @@ Run "go help get" for more information. All flags available for "go get" is also }) return nil } else { - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil)) + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, nil)) if err != nil { return err } @@ -281,7 +300,6 @@ Run "go help get" for more information. All flags available for "go get" is also npmCommand, }, } - } type modCommands struct { @@ -299,18 +317,18 @@ func (c *modCommands) Name() string { } func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - _, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil) + _, err := c.r.ConfigFromProvider(configKey{counter: c.r.configVersionID.Load()}, nil) if err != nil { return err } - //config := conf.configs.Base + // config := conf.configs.Base return nil } func (c *modCommands) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "Various Hugo Modules helpers." + cmd.Short = "Manage modules" cmd.Long = `Various helpers to help manage the modules in your project's dependency graph. Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git). This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor". diff --git a/commands/new.go b/commands/new.go index 6760bf719..81e1c65a4 100644 --- a/commands/new.go +++ b/commands/new.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -16,19 +16,14 @@ package commands import ( "bytes" "context" - "errors" - "fmt" "path/filepath" "strings" "github.com/bep/simplecobra" - "github.com/gohugoio/hugo/common/htime" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/create" - "github.com/gohugoio/hugo/helpers" - "github.com/gohugoio/hugo/parser" - "github.com/gohugoio/hugo/parser/metadecoders" - "github.com/spf13/afero" + "github.com/gohugoio/hugo/create/skeletons" "github.com/spf13/cobra" ) @@ -45,18 +40,18 @@ func newNewCommand() *newCommand { &simpleCommand{ name: "content", use: "content [path]", - short: "Create new content for your site", + short: "Create new content", 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 { @@ -65,24 +60,27 @@ func newNewCommand() *newCommand { return create.NewContent(h, contentType, args[0], force) }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } + return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs + } cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create") cmd.Flags().String("editor", "", "edit new content with this editor, if provided") + _ = cmd.RegisterFlagCompletionFunc("editor", cobra.NoFileCompletions) cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists") - cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)") applyLocalFlagsBuildConfig(cmd, r) - }, }, &simpleCommand{ name: "site", use: "site [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.`, + short: "Create a new site", + long: `Create a new site at the specified path.`, 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 { @@ -93,166 +91,77 @@ Use ` + "`hugo new [contentPath]`" + ` to create new content.`, cfg.Set("workingDir", createpath) cfg.Set("publishDir", "public") - conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg)) + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg)) if err != nil { return err } sourceFs := conf.fs.Source - archeTypePath := filepath.Join(createpath, "archetypes") - dirs := []string{ - archeTypePath, - filepath.Join(createpath, "assets"), - filepath.Join(createpath, "content"), - filepath.Join(createpath, "data"), - filepath.Join(createpath, "layouts"), - filepath.Join(createpath, "static"), - filepath.Join(createpath, "themes"), + err = skeletons.CreateSite(createpath, sourceFs, force, format) + if err != nil { + return err } - if exists, _ := helpers.Exists(createpath, sourceFs); exists { - if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir { - return errors.New(createpath + " already exists but not a directory") - } - - isEmpty, _ := helpers.IsEmpty(createpath, sourceFs) - - switch { - case !isEmpty && !force: - return errors.New(createpath + " already exists and is not empty. See --force.") - - case !isEmpty && force: - all := append(dirs, filepath.Join(createpath, "hugo."+format)) - for _, path := range all { - if exists, _ := helpers.Exists(path, sourceFs); exists { - return errors.New(path + " already exists") - } - } - } - } - - for _, dir := range dirs { - if err := sourceFs.MkdirAll(dir, 0777); err != nil { - return fmt.Errorf("failed to create dir: %w", err) - } - } - - c.newSiteCreateConfig(sourceFs, createpath, format) - - // Create a default archetype file. - helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"), - strings.NewReader(create.DefaultArchetypeTemplateTemplate), sourceFs) - - r.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", createpath) - r.Println(c.newSiteNextStepsText()) + r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath) + r.Println(c.newSiteNextStepsText(createpath, format)) return nil }, withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } + return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs + } 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)") + _ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) }, }, &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", + long: `Create a new theme with the specified name in the ./themes directory. +This generates a functional theme including template examples and sample content.`, run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - h, err := r.Hugo(flagsToCfg(cd, nil)) + if len(args) < 1 { + return newUserError("theme name needs to be provided") + } + cfg := config.New() + cfg.Set("publishDir", "public") + + conf, err := r.ConfigFromProvider(configKey{counter: r.configVersionID.Load()}, flagsToCfg(cd, cfg)) if err != nil { return err } - ps := h.PathSpec - sourceFs := ps.Fs.Source - themesDir := h.Configs.LoadingInfo.BaseConfig.ThemesDir - createpath := ps.AbsPathify(filepath.Join(themesDir, args[0])) - r.Println("Creating theme at", createpath) + sourceFs := conf.fs.Source + createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0])) + r.Println("Creating new theme in", createpath) - if x, _ := helpers.Exists(createpath, sourceFs); x { - return errors.New(createpath + " already exists") - } - - for _, filename := range []string{ - "index.html", - "404.html", - "_default/list.html", - "_default/single.html", - "partials/head.html", - "partials/header.html", - "partials/footer.html", - } { - touchFile(sourceFs, filepath.Join(createpath, "layouts", filename)) - } - - baseofDefault := []byte(` - - {{- partial "head.html" . -}} - - {{- partial "header.html" . -}} -
- {{- block "main" . }}{{- end }} -
- {{- partial "footer.html" . -}} - - -`) - - err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), sourceFs) + err = skeletons.CreateTheme(createpath, sourceFs, format) if err != nil { return err } - mkdir(createpath, "archetypes") - - archDefault := []byte("+++\n+++\n") - - err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), sourceFs) - if err != nil { - return err - } - - mkdir(createpath, "static", "js") - mkdir(createpath, "static", "css") - - by := []byte(`The MIT License (MIT) - -Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -`) - - err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), sourceFs) - if err != nil { - return err - } - - c.createThemeMD(ps.Fs.Source, createpath) - return nil }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return []string{}, cobra.ShellCompDirectiveNoFileComp + } + return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs + } + cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)") + _ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp)) + }, }, }, } return c - } type newCommand struct { @@ -275,7 +184,7 @@ func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args [ func (c *newCommand) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "Create new content for your site" + cmd.Short = "Create new content" cmd.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. @@ -284,6 +193,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 } @@ -292,77 +203,25 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error { return nil } -func (c *newCommand) newSiteCreateConfig(fs afero.Fs, inpath string, kind string) (err error) { - in := map[string]string{ - "baseURL": "http://example.org/", - "title": "My New Hugo Site", - "languageCode": "en-us", - } - - var buf bytes.Buffer - err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf) - if err != nil { - return err - } - - return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+kind), &buf, fs) -} - -func (c *newCommand) newSiteNextStepsText() string { +func (c *newCommand) newSiteNextStepsText(path string, format string) string { + format = strings.ToLower(format) var nextStepsText bytes.Buffer - nextStepsText.WriteString(`Just a few more steps and you're ready to go: + nextStepsText.WriteString(`Just a few more steps... -1. Download a theme into the same-named folder. - Choose a theme from https://themes.gohugo.io/ or - create your own with the "hugo new theme " command. -2. Perhaps you want to add some content. You can add single files - with "hugo new `) +1. Change the current directory to ` + path + `. +2. Create or install a theme: + - Create a new theme with the command "hugo new theme " + - Or, install a theme from https://themes.gohugo.io/ +3. Edit hugo.` + format + `, setting the "theme" property to the theme name. +4. Create new content with the command "hugo new content `) nextStepsText.WriteString(filepath.Join("", ".")) nextStepsText.WriteString(`". -3. Start the built-in live server via "hugo server". +5. Start the embedded web server with the command "hugo server --buildDrafts". -Visit https://gohugo.io/ for quickstart guide and full documentation.`) +See documentation at https://gohugo.io/.`) return nextStepsText.String() } - -func (c *newCommand) createThemeMD(fs afero.Fs, inpath string) (err error) { - - by := []byte(`# theme.toml template for a Hugo theme -# See https://github.com/gohugoio/hugoThemes#themetoml for an example - -name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `" -license = "MIT" -licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE" -description = "" -homepage = "http://example.com/" -tags = [] -features = [] -min_version = "0.112.0" - -[author] - name = "" - homepage = "" - -# If porting an existing theme -[original] - name = "" - homepage = "" - repo = "" -`) - - err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs) - if err != nil { - return - } - - err = helpers.WriteToDisk(filepath.Join(inpath, "hugo.toml"), strings.NewReader("# Theme config.\n"), fs) - if err != nil { - return - } - - return nil -} diff --git a/commands/release.go b/commands/release.go index 54cf936e8..059f04eb8 100644 --- a/commands/release.go +++ b/commands/release.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -24,7 +24,6 @@ import ( // Note: This is a command only meant for internal use and must be run // via "go run -tags release main.go release" on the actual code base that is in the release. func newReleaseCommand() simplecobra.Commander { - var ( step int skipPush bool @@ -33,7 +32,7 @@ func newReleaseCommand() simplecobra.Commander { return &simpleCommand{ name: "release", - short: "Release a new version of Hugo.", + short: "Release a new version of Hugo", run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { rel, err := releaser.New(skipPush, try, step) if err != nil { @@ -44,9 +43,11 @@ func newReleaseCommand() simplecobra.Commander { }, withc: func(cmd *cobra.Command, r *rootCommand) { cmd.Hidden = true + cmd.ValidArgsFunction = cobra.NoFileCompletions cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote") cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes") cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)") + _ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp)) }, } } diff --git a/commands/server.go b/commands/server.go index c878cac2f..c8895b9a1 100644 --- a/commands/server.go +++ b/commands/server.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -16,52 +16,59 @@ package commands import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" - "io/ioutil" + "maps" "net" "net/http" + _ "net/http/pprof" "net/url" "os" - "sync" - "sync/atomic" - "os/signal" "path" "path/filepath" "regexp" + "sort" "strconv" "strings" + "sync" + "sync/atomic" "syscall" "time" + "github.com/bep/mclib" + "github.com/pkg/browser" + "github.com/bep/debounce" "github.com/bep/simplecobra" "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/tpl/tplimpl" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib/filesystems" "github.com/gohugoio/hugo/livereload" - "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/transform" "github.com/gohugoio/hugo/transform/livereloadinject" "github.com/spf13/afero" + "github.com/spf13/cobra" "github.com/spf13/fsync" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" ) var ( - logErrorRe = regexp.MustCompile(`(?s)ERROR \d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} `) logDuplicateTemplateExecuteRe = regexp.MustCompile(`: template: .*?:\d+:\d+: executing ".*?"`) logDuplicateTemplateParseRe = regexp.MustCompile(`: template: .*?:\d+:\d*`) ) @@ -78,11 +85,19 @@ const ( configChangeGoWork = "go work file" ) +const ( + hugoHeaderRedirect = "X-Hugo-Redirect" +) + func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(reloaded bool) error) *hugoBuilder { + var visitedURLs *types.EvictingQueue[string] + if s != nil && !s.disableFastRender { + visitedURLs = types.NewEvictingQueue[string](20) + } return &hugoBuilder{ r: r, s: s, - visitedURLs: types.NewEvictingStringQueue(100), + visitedURLs: visitedURLs, fullRebuildSem: semaphore.NewWeighted(1), debounce: debounce.New(4 * time.Second), onConfigLoaded: func(reloaded bool) error { @@ -97,13 +112,38 @@ func newHugoBuilder(r *rootCommand, s *serverCommand, onConfigLoaded ...func(rel } func newServerCommand() *serverCommand { - var c *serverCommand - c = &serverCommand{ + // Flags. + var uninstall bool + + c := &serverCommand{ quit: make(chan bool), + commands: []simplecobra.Commander{ + &simpleCommand{ + name: "trust", + short: "Install the local CA in the system trust store", + run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { + action := "-install" + if uninstall { + action = "-uninstall" + } + os.Args = []string{action} + return mclib.RunMain() + }, + withc: func(cmd *cobra.Command, r *rootCommand) { + cmd.ValidArgsFunction = cobra.NoFileCompletions + cmd.Flags().BoolVar(&uninstall, "uninstall", false, "Uninstall the local CA (but do not delete it).") + }, + }, + }, } + return c } +func (c *serverCommand) Commands() []simplecobra.Commander { + return c.commands +} + type countingStatFs struct { afero.Fs statCounter uint64 @@ -129,16 +169,16 @@ type dynamicEvents struct { type fileChangeDetector struct { sync.Mutex - current map[string]string - prev map[string]string + current map[string]uint64 + prev map[string]uint64 irrelevantRe *regexp.Regexp } -func (f *fileChangeDetector) OnFileClose(name, md5sum string) { +func (f *fileChangeDetector) OnFileClose(name string, checksum uint64) { f.Lock() defer f.Unlock() - f.current[name] = md5sum + f.current[name] = checksum } func (f *fileChangeDetector) PrepareNew() { @@ -150,16 +190,14 @@ func (f *fileChangeDetector) PrepareNew() { defer f.Unlock() if f.current == nil { - f.current = make(map[string]string) - f.prev = make(map[string]string) + f.current = make(map[string]uint64) + f.prev = make(map[string]uint64) return } - f.prev = make(map[string]string) - for k, v := range f.current { - f.prev[k] = v - } - f.current = make(map[string]string) + f.prev = make(map[string]uint64) + maps.Copy(f.prev, f.current) + f.current = make(map[string]uint64) } func (f *fileChangeDetector) changed() []string { @@ -176,21 +214,22 @@ func (f *fileChangeDetector) changed() []string { } } - return f.filterIrrelevant(c) + return f.filterIrrelevantAndSort(c) } -func (f *fileChangeDetector) filterIrrelevant(in []string) []string { +func (f *fileChangeDetector) filterIrrelevantAndSort(in []string) []string { var filtered []string for _, v := range in { if !f.irrelevantRe.MatchString(v) { filtered = append(filtered, v) } } + sort.Strings(filtered) return filtered } type fileServer struct { - baseURLs []string + baseURLs []urls.BaseURL roots []string errorTemplate func(err any) (io.Reader, error) c *serverCommand @@ -204,15 +243,16 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string listener := f.c.serverPorts[i].ln logger := f.c.r.logger - r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment) - if i == 0 { - if f.c.renderToDisk { - r.Println("Serving pages from disk") - } else if f.c.renderStaticToDisk { - r.Println("Serving pages from memory and static files from disk") + r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment) + mainTarget := "disk" + if f.c.r.renderToMemory { + mainTarget = "memory" + } + if f.c.renderStaticToDisk { + r.Printf("Serving pages from %s and static files from disk\n", mainTarget) } else { - r.Println("Serving pages from memory") + r.Printf("Serving pages from %s\n", mainTarget) } } @@ -226,12 +266,6 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string r.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") } - // We're only interested in the path - u, err := url.Parse(baseURL) - if err != nil { - return nil, nil, "", "", fmt.Errorf("invalid baseURL: %w", err) - } - decorate := func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if f.c.showErrorInBrowser { @@ -251,7 +285,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string port = lrport } }) - lr := *u + lr := baseURL.URL() lr.Host = fmt.Sprintf("%s:%d", lr.Hostname(), port) fmt.Fprint(w, injectLiveReloadScript(r, lr)) @@ -276,64 +310,65 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string w.Header().Set(header.Key, header.Value) } - if redirect := serverConfig.MatchRedirect(requestURI); !redirect.IsZero() { - // fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) - doRedirect := true - // This matches Netlify's behaviour and is needed for SPA behaviour. - // See https://docs.netlify.com/routing/redirects/rewrites-proxies/ - if !redirect.Force { - path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path)) - if root != "" { - path = filepath.Join(root, path) - } - var fs afero.Fs - f.c.withConf(func(conf *commonConfig) { - fs = conf.fs.PublishDirServer - }) - - fi, err := fs.Stat(path) - - if err == nil { - if fi.IsDir() { - // There will be overlapping directories, so we - // need to check for a file. - _, err = fs.Stat(filepath.Join(path, "index.html")) - doRedirect = err != nil - } else { - doRedirect = false + if canRedirect(requestURI, r) { + if redirect := serverConfig.MatchRedirect(requestURI, r.Header); !redirect.IsZero() { + doRedirect := true + // This matches Netlify's behavior and is needed for SPA behavior. + // See https://docs.netlify.com/routing/redirects/rewrites-proxies/ + if !redirect.Force { + path := filepath.Clean(strings.TrimPrefix(requestURI, baseURL.Path())) + if root != "" { + path = filepath.Join(root, path) } - } - } + var fs afero.Fs + f.c.withConf(func(conf *commonConfig) { + fs = conf.fs.PublishDirServer + }) + + fi, err := fs.Stat(path) - if doRedirect { - switch redirect.Status { - case 404: - w.WriteHeader(404) - file, err := fs.Open(strings.TrimPrefix(redirect.To, u.Path)) if err == nil { - defer file.Close() - io.Copy(w, file) - } else { - fmt.Fprintln(w, "

Page Not Found

") + if fi.IsDir() { + // There will be overlapping directories, so we + // need to check for a file. + _, err = fs.Stat(filepath.Join(path, "index.html")) + doRedirect = err != nil + } else { + doRedirect = false + } } - return - case 200: - if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil { - requestURI = redirect.To - r = r2 - } - default: - w.Header().Set("Content-Type", "") - http.Redirect(w, r, redirect.To, redirect.Status) - return + } + if doRedirect { + w.Header().Set(hugoHeaderRedirect, "true") + switch redirect.Status { + case 404: + w.WriteHeader(404) + file, err := fs.Open(strings.TrimPrefix(redirect.To, baseURL.Path())) + if err == nil { + defer file.Close() + io.Copy(w, file) + } else { + fmt.Fprintln(w, "

Page Not Found

") + } + return + case 200: + if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, baseURL.Path())); r2 != nil { + requestURI = redirect.To + r = r2 + } + default: + w.Header().Set("Content-Type", "") + http.Redirect(w, r, redirect.To, redirect.Status) + return + + } } } - } if f.c.fastRenderMode && f.c.errState.buildErr() == nil { - if strings.HasSuffix(requestURI, "/") || strings.HasSuffix(requestURI, "html") || strings.HasSuffix(requestURI, "htm") { + if isNavigation(requestURI, r) { if !f.c.visitedURLs.Contains(requestURI) { // If not already on stack, re-render that single page. if err := f.c.partialReRender(requestURI); err != nil { @@ -356,10 +391,10 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string fileserver := decorate(http.FileServer(fs)) mu := http.NewServeMux() - if u.Path == "" || u.Path == "/" { + if baseURL.Path() == "" || baseURL.Path() == "/" { mu.Handle("/", fileserver) } else { - mu.Handle(u.Path, http.StripPrefix(u.Path, fileserver)) + mu.Handle(baseURL.Path(), http.StripPrefix(baseURL.Path(), fileserver)) } if r.IsTestRun() { var shutDownOnce sync.Once @@ -372,7 +407,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string endpoint := net.JoinHostPort(f.c.serverInterface, strconv.Itoa(port)) - return mu, listener, u.String(), endpoint, nil + return mu, listener, baseURL.String(), endpoint, nil } func (f *fileServer) rewriteRequest(r *http.Request, toPath string) *http.Request { @@ -418,11 +453,15 @@ type serverCommand struct { doLiveReload bool // Flags. - renderToDisk bool renderStaticToDisk bool navigateToChanged bool + openBrowser bool serverAppend bool serverInterface string + tlsCertFile string + tlsKeyFile string + tlsAuto bool + pprof bool serverPort int liveReloadPort int serverWatch bool @@ -432,24 +471,16 @@ type serverCommand struct { disableBrowserError bool } -func (c *serverCommand) Commands() []simplecobra.Commander { - return c.commands -} - func (c *serverCommand) Name() string { return "server" } func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - err := func() error { - defer c.r.timeTrack(time.Now(), "Built") - err := c.build() - return err - }() - if err != nil { - return err + if c.pprof { + go func() { + http.ListenAndServe("localhost:8080", nil) + }() } - // Watch runs its own server as part of the routine if c.serverWatch { @@ -472,19 +503,26 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg } + err := func() error { + defer c.r.timeTrack(time.Now(), "Built") + return c.build() + }() + if err != nil { + return err + } + return c.serve() } func (c *serverCommand) Init(cd *simplecobra.Commandeer) error { cmd := cd.CobraCommand - cmd.Short = "A high performance webserver" + cmd.Short = "Start the embedded web server" cmd.Long = `Hugo provides its own webserver which builds and serves the site. While hugo server is high performance, it is a webserver with limited options. -Many run it in production, but the standard behavior is for people to use it -in development and use a more full featured server such as Nginx or Caddy. -'hugo server' will avoid writing the rendered and served content to disk, -preferring to store it in memory. +The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but +you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` flag. This can be +faster in some cases, but it will consume more memory. By default hugo will also watch your files for any changes you make and automatically rebuild the site. It will then live reload any open browser pages @@ -493,21 +531,27 @@ of a second, you will be able to save and see your changes nearly instantly.` cmd.Aliases = []string{"serve"} cmd.Flags().IntVarP(&c.serverPort, "port", "p", 1313, "port on which the server will listen") + _ = cmd.RegisterFlagCompletionFunc("port", cobra.NoFileCompletions) cmd.Flags().IntVar(&c.liveReloadPort, "liveReloadPort", -1, "port for live reloading (i.e. 443 in HTTPS proxy situations)") + _ = cmd.RegisterFlagCompletionFunc("liveReloadPort", cobra.NoFileCompletions) cmd.Flags().StringVarP(&c.serverInterface, "bind", "", "127.0.0.1", "interface to which the server will bind") + _ = cmd.RegisterFlagCompletionFunc("bind", cobra.NoFileCompletions) + cmd.Flags().StringVarP(&c.tlsCertFile, "tlsCertFile", "", "", "path to TLS certificate file") + _ = cmd.MarkFlagFilename("tlsCertFile", "pem") + cmd.Flags().StringVarP(&c.tlsKeyFile, "tlsKeyFile", "", "", "path to TLS key file") + _ = cmd.MarkFlagFilename("tlsKeyFile", "pem") + cmd.Flags().BoolVar(&c.tlsAuto, "tlsAuto", false, "generate and use locally-trusted certificates.") + cmd.Flags().BoolVar(&c.pprof, "pprof", false, "enable the pprof server (port 8080)") cmd.Flags().BoolVarP(&c.serverWatch, "watch", "w", true, "watch filesystem for changes and recreate as needed") cmd.Flags().BoolVar(&c.noHTTPCache, "noHTTPCache", false, "prevent HTTP caching") cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL") cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild") - cmd.Flags().BoolVar(&c.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") - cmd.Flags().BoolVar(&c.renderToDisk, "renderToDisk", false, "serve all files from disk (default is from memory)") + cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload") + cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site in a browser after server startup") cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory") cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes") cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser") - cmd.Flags().String("memstats", "", "log memory usage to this file") - cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") - r := cd.Root.Command.(*rootCommand) applyLocalFlagsBuild(cmd, r) @@ -525,8 +569,15 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error { if err := c.createServerPorts(cd); err != nil { return err } + + if (c.tlsCertFile == "" || c.tlsKeyFile == "") && c.tlsAuto { + c.withConfE(func(conf *commonConfig) error { + return c.createCertificates(conf) + }) + } } - if err := c.setBaseURLsInConfig(); err != nil { + + if err := c.setServerInfoInConfig(); err != nil { return err } @@ -542,7 +593,9 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error { ) destinationFlag := cd.CobraCommand.Flags().Lookup("destination") - c.renderToDisk = c.renderToDisk || (destinationFlag != nil && destinationFlag.Changed) + if c.r.renderToMemory && (destinationFlag != nil && destinationFlag.Changed) { + return fmt.Errorf("cannot use --renderToMemory with --destination") + } c.doLiveReload = !c.disableLiveReload c.fastRenderMode = !c.disableFastRender c.showErrorInBrowser = c.doLiveReload && !c.disableBrowserError @@ -569,15 +622,15 @@ func (c *serverCommand) PreRun(cd, runner *simplecobra.Commandeer) error { return nil } -func (c *serverCommand) setBaseURLsInConfig() error { +func (c *serverCommand) setServerInfoInConfig() error { if len(c.serverPorts) == 0 { panic("no server ports set") } return c.withConfE(func(conf *commonConfig) error { - for i, language := range conf.configs.Languages { - isMultiHost := conf.configs.IsMultihost + for i, language := range conf.configs.LanguagesDefaultFirst { + isMultihost := conf.configs.IsMultihost var serverPort int - if isMultiHost { + if isMultihost { serverPort = c.serverPorts[i].p } else { serverPort = c.serverPorts[0].p @@ -596,37 +649,108 @@ func (c *serverCommand) setBaseURLsInConfig() error { if c.liveReloadPort != -1 { baseURLLiveReload, _ = baseURLLiveReload.WithPort(c.liveReloadPort) } - langConfig.C.SetBaseURL(baseURL, baseURLLiveReload) + langConfig.C.SetServerInfo(baseURL, baseURLLiveReload, c.serverInterface) + } return nil }) } func (c *serverCommand) getErrorWithContext() any { - errCount := c.errCount() - - if errCount == 0 { + buildErr := c.errState.buildErr() + if buildErr == nil { return nil } m := make(map[string]any) - //xwm["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors()))) - m["Error"] = errors.New(cleanErrorLog(removeErrorPrefixFromLog(c.r.logger.Errors()))) + m["Error"] = cleanErrorLog(c.r.logger.Errors()) + m["Version"] = hugo.BuildVersionString() - ferrors := herrors.UnwrapFileErrorsWithErrorContext(c.errState.buildErr()) + ferrors := herrors.UnwrapFileErrorsWithErrorContext(buildErr) m["Files"] = ferrors return m } +func (c *serverCommand) createCertificates(conf *commonConfig) error { + hostname := "localhost" + if c.r.baseURL != "" { + u, err := url.Parse(c.r.baseURL) + if err != nil { + return err + } + hostname = u.Hostname() + } + + // For now, store these in the Hugo cache dir. + // Hugo should probably introduce some concept of a less temporary application directory. + keyDir := filepath.Join(conf.configs.LoadingInfo.BaseConfig.CacheDir, "_mkcerts") + + // Create the directory if it doesn't exist. + if _, err := os.Stat(keyDir); os.IsNotExist(err) { + if err := os.MkdirAll(keyDir, 0o777); err != nil { + return err + } + } + + c.tlsCertFile = filepath.Join(keyDir, fmt.Sprintf("%s.pem", hostname)) + c.tlsKeyFile = filepath.Join(keyDir, fmt.Sprintf("%s-key.pem", hostname)) + + // Check if the certificate already exists and is valid. + certPEM, err := os.ReadFile(c.tlsCertFile) + if err == nil { + rootPem, err := os.ReadFile(filepath.Join(mclib.GetCAROOT(), "rootCA.pem")) + if err == nil { + if err := c.verifyCert(rootPem, certPEM, hostname); err == nil { + c.r.Println("Using existing", c.tlsCertFile, "and", c.tlsKeyFile) + return nil + } + } + } + + c.r.Println("Creating TLS certificates in", keyDir) + + // Yes, this is unfortunate, but it's currently the only way to use Mkcert as a library. + os.Args = []string{"-cert-file", c.tlsCertFile, "-key-file", c.tlsKeyFile, hostname} + return mclib.RunMain() +} + +func (c *serverCommand) verifyCert(rootPEM, certPEM []byte, name string) error { + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(rootPEM) + if !ok { + return fmt.Errorf("failed to parse root certificate") + } + + block, _ := pem.Decode(certPEM) + if block == nil { + return fmt.Errorf("failed to parse certificate PEM") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse certificate: %v", err.Error()) + } + + opts := x509.VerifyOptions{ + DNSName: name, + Roots: roots, + } + + if _, err := cert.Verify(opts); err != nil { + return fmt.Errorf("failed to verify certificate: %v", err.Error()) + } + + return nil +} + func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error { flags := cd.CobraCommand.Flags() var cerr error c.withConf(func(conf *commonConfig) { - isMultiHost := conf.configs.IsMultihost + isMultihost := conf.configs.IsMultihost c.serverPorts = make([]serverPortListener, 1) - if isMultiHost { + if isMultihost { if !c.serverAppend { cerr = errors.New("--appendPort=false not supported when in multihost mode") return @@ -634,7 +758,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error { c.serverPorts = make([]serverPortListener, len(conf.configs.Languages)) } currentServerPort := c.serverPort - for i := 0; i < len(c.serverPorts); i++ { + for i := range c.serverPorts { l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort))) if err == nil { c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort} @@ -662,36 +786,40 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error { // fixURL massages the baseURL into a form needed for serving // all pages correctly. -func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) { +func (c *serverCommand) fixURL(baseURLFromConfig, baseURLFromFlag string, port int) (string, error) { + certsSet := (c.tlsCertFile != "" && c.tlsKeyFile != "") || c.tlsAuto useLocalhost := false - if s == "" { - s = baseURL + baseURL := baseURLFromFlag + if baseURL == "" { + baseURL = baseURLFromConfig useLocalhost = true } - if !strings.HasSuffix(s, "/") { - s = s + "/" + if !strings.HasSuffix(baseURL, "/") { + baseURL = baseURL + "/" } // do an initial parse of the input string - u, err := url.Parse(s) + u, err := url.Parse(baseURL) if err != nil { return "", err } // if no Host is defined, then assume that no schema or double-slash were // present in the url. Add a double-slash and make a best effort attempt. - if u.Host == "" && s != "/" { - s = "//" + s + if u.Host == "" && baseURL != "/" { + baseURL = "//" + baseURL - u, err = url.Parse(s) + u, err = url.Parse(baseURL) if err != nil { return "", err } } if useLocalhost { - if u.Scheme == "https" { + if certsSet { + u.Scheme = "https" + } else if u.Scheme == "https" { u.Scheme = "http" } u.Host = "localhost" @@ -710,52 +838,57 @@ func (c *serverCommand) fixURL(baseURL, s string, port int) (string, error) { return u.String(), nil } -func (c *serverCommand) partialReRender(urls ...string) error { +func (c *serverCommand) partialReRender(urls ...string) (err error) { defer func() { c.errState.setWasErr(false) }() - c.errState.setBuildErr(nil) - visited := make(map[string]bool) + visited := types.NewEvictingQueue[string](len(urls)) for _, url := range urls { - visited[url] = true + visited.Add(url) } - h, err := c.hugo() + var h *hugolib.HugoSites + h, err = c.hugo() if err != nil { - return err + return } + // Note: We do not set NoBuildLock as the file lock is not acquired at this stage. - return h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()}) + err = h.Build(hugolib.BuildCfg{NoBuildLock: false, RecentlyTouched: visited, PartialReRender: true, ErrRecovery: c.errState.wasErr()}) + + return } func (c *serverCommand) serve() error { var ( - baseURLs []string + baseURLs []urls.BaseURL roots []string h *hugolib.HugoSites ) err := c.withConfE(func(conf *commonConfig) error { - isMultiHost := conf.configs.IsMultihost + isMultihost := conf.configs.IsMultihost var err error h, err = c.r.HugFromConfig(conf) if err != nil { return err } - if isMultiHost { + // We need the server to share the same logger as the Hugo build (for error counts etc.) + c.r.logger = h.Log + + if isMultihost { for _, l := range conf.configs.ConfigLangs() { - baseURLs = append(baseURLs, l.BaseURL().String()) + baseURLs = append(baseURLs, l.BaseURL()) roots = append(roots, l.Language().Lang) } } else { l := conf.configs.GetFirstLanguageConfig() - baseURLs = []string{l.BaseURL().String()} + baseURLs = []urls.BaseURL{l.BaseURL()} roots = []string{""} } return nil }) - if err != nil { return err } @@ -764,16 +897,16 @@ func (c *serverCommand) serve() error { // To allow the en user to change the error template while the server is running, we use // the freshest template we can provide. var ( - errTempl tpl.Template - templHandler tpl.TemplateHandler + errTempl *tplimpl.TemplInfo + templHandler *tplimpl.TemplateStore ) - getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) { + getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) { if h == nil { return errTempl, templHandler } - templHandler := h.Tmpl() - errTempl, found := templHandler.Lookup("_server/error.html") - if !found { + templHandler := h.GetTemplateStore() + errTempl := templHandler.LookupByPath("/_server/error.html") + if errTempl == nil { panic("template server/error.html not found") } return errTempl, templHandler @@ -808,24 +941,36 @@ func (c *serverCommand) serve() error { for i := range baseURLs { mu, listener, serverURL, endpoint, err := srv.createEndpoint(i) - srv := &http.Server{ - Addr: endpoint, - Handler: mu, + var srv *http.Server + if c.tlsCertFile != "" && c.tlsKeyFile != "" { + srv = &http.Server{ + Addr: endpoint, + Handler: mu, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + } + } else { + srv = &http.Server{ + Addr: endpoint, + Handler: mu, + } } + servers = append(servers, srv) if doLiveReload { - u, err := url.Parse(helpers.SanitizeURL(baseURLs[i])) - if err != nil { - return err - } - - mu.HandleFunc(u.Path+"/livereload.js", livereload.ServeJS) - mu.HandleFunc(u.Path+"/livereload", livereload.Handler) + baseURL := baseURLs[i] + mu.HandleFunc(baseURL.Path()+"livereload.js", livereload.ServeJS) + mu.HandleFunc(baseURL.Path()+"livereload", livereload.Handler) } - c.r.Printf("Web Server is available at %s (bind address %s)\n", serverURL, c.serverInterface) + c.r.Printf("Web Server is available at %s (bind address %s) %s\n", serverURL, c.serverInterface, roots[i]) wg1.Go(func() error { - err = srv.Serve(listener) + if c.tlsCertFile != "" && c.tlsKeyFile != "" { + err = srv.ServeTLS(listener, c.tlsCertFile, c.tlsKeyFile) + } else { + err = srv.Serve(listener) + } if err != nil && err != http.ErrServerClosed { return err } @@ -836,8 +981,12 @@ func (c *serverCommand) serve() error { if c.r.IsTestRun() { // Write a .ready file to disk to signal ready status. // This is where the test is run from. + var baseURLs []string + for _, baseURL := range srv.baseURLs { + baseURLs = append(baseURLs, baseURL.String()) + } testInfo := map[string]any{ - "baseURLs": srv.baseURLs, + "baseURLs": baseURLs, } dir := os.Getenv("WORK") @@ -848,7 +997,7 @@ func (c *serverCommand) serve() error { if err != nil { return err } - err = ioutil.WriteFile(readyFile, b, 0777) + err = os.WriteFile(readyFile, b, 0o777) if err != nil { return err } @@ -858,6 +1007,13 @@ func (c *serverCommand) serve() error { c.r.Println("Press Ctrl+C to stop") + if c.openBrowser { + // There may be more than one baseURL in multihost mode, open the first. + if err := browser.OpenURL(baseURLs[0].String()); err != nil { + c.r.logger.Warnf("Failed to open browser: %s", err) + } + } + err = func() error { for { select { @@ -870,15 +1026,10 @@ func (c *serverCommand) serve() error { } } }() - if err != nil { c.r.Println("Error:", err) } - if h := c.hugoTry(); h != nil { - h.Close() - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() wg2, ctx := errgroup.WithContext(ctx) @@ -931,8 +1082,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { } }) - // prevent spamming the log on changes - logger := helpers.NewDistinctErrorLogger() + logger := s.c.r.logger for _, ev := range staticEvents { // Due to our approach of layering both directories and the content's rendered output @@ -953,7 +1103,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { fromPath := ev.Name - relPath, found := sourceFs.MakePathRelative(fromPath) + relPath, found := sourceFs.MakePathRelative(fromPath, true) if !found { // Not member of this virtual host. @@ -1033,7 +1183,7 @@ func cleanErrorLog(content string) string { return strings.Join(keep, ": ") } -func injectLiveReloadScript(src io.Reader, baseURL url.URL) string { +func injectLiveReloadScript(src io.Reader, baseURL *url.URL) string { var b bytes.Buffer chain := transform.Chain{livereloadinject.New(baseURL)} chain.Apply(&b, src) @@ -1052,16 +1202,16 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs return } -func pickOneWriteOrCreatePath(events []fsnotify.Event) string { +func pickOneWriteOrCreatePath(contentTypes config.ContentTypesProvider, events []fsnotify.Event) string { name := "" for _, ev := range events { if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create { - if files.IsIndexContentFile(ev.Name) { + if contentTypes.IsIndexContentFile(ev.Name) { return ev.Name } - if files.IsContentFile(ev.Name) { + if contentTypes.IsContentFile(ev.Name) { name = ev.Name } @@ -1071,10 +1221,6 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string { return name } -func removeErrorPrefixFromLog(content string) string { - return logErrorRe.ReplaceAllLiteralString(content, "") -} - func formatByteCount(b uint64) string { const unit = 1000 if b < unit { @@ -1088,3 +1234,24 @@ func formatByteCount(b uint64) string { return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) } + +func canRedirect(requestURIWithoutQuery string, r *http.Request) bool { + if r.Header.Get(hugoHeaderRedirect) != "" { + return false + } + return isNavigation(requestURIWithoutQuery, r) +} + +// Sec-Fetch-Mode should be sent by all recent browser versions, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode#navigate +// Fall back to the file extension if not set. +// The main take here is that we don't want to have CSS/JS files etc. partake in this logic. +func isNavigation(requestURIWithoutQuery string, r *http.Request) bool { + return r.Header.Get("Sec-Fetch-Mode") == "navigate" || isPropablyHTMLRequest(requestURIWithoutQuery) +} + +func isPropablyHTMLRequest(requestURIWithoutQuery string) bool { + if strings.HasSuffix(requestURIWithoutQuery, "/") || strings.HasSuffix(requestURIWithoutQuery, "html") || strings.HasSuffix(requestURIWithoutQuery, "htm") { + return true + } + return !strings.Contains(requestURIWithoutQuery, ".") +} diff --git a/commands/xcommand_template.go b/commands/xcommand_template.go deleted file mode 100644 index eeb9409a0..000000000 --- a/commands/xcommand_template.go +++ /dev/null @@ -1,79 +0,0 @@ -// 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 commands - -import ( - "context" - "fmt" - - "github.com/bep/simplecobra" - "github.com/spf13/cobra" -) - -func newSimpleTemplateCommand() simplecobra.Commander { - return &simpleCommand{ - name: "template", - run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error { - - return nil - }, - withc: func(cmd *cobra.Command, r *rootCommand) { - - }, - } - -} - -func newTemplateCommand() *templateCommand { - return &templateCommand{ - commands: []simplecobra.Commander{}, - } - -} - -type templateCommand struct { - r *rootCommand - - commands []simplecobra.Commander -} - -func (c *templateCommand) Commands() []simplecobra.Commander { - return c.commands -} - -func (c *templateCommand) Name() string { - return "template" -} - -func (c *templateCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { - conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil)) - if err != nil { - return err - } - fmt.Println("templateCommand.Run", conf) - - return nil -} - -func (c *templateCommand) Init(cd *simplecobra.Commandeer) error { - cmd := cd.CobraCommand - cmd.Short = "Print the site configuration" - cmd.Long = `Print the site configuration, both default and custom settings.` - return nil -} - -func (c *templateCommand) PreRun(cd, runner *simplecobra.Commandeer) error { - c.r = cd.Root.Command.(*rootCommand) - return nil -} diff --git a/common/collections/append.go b/common/collections/append.go index a9c14c1aa..db9db8bf3 100644 --- a/common/collections/append.go +++ b/common/collections/append.go @@ -22,29 +22,64 @@ import ( // If length of from is one and the only element is a slice of same type as to, // it will be appended. func Append(to any, from ...any) (any, error) { + if len(from) == 0 { + return to, nil + } tov, toIsNil := indirect(reflect.ValueOf(to)) toIsNil = toIsNil || to == nil var tot reflect.Type if !toIsNil { + if tov.Kind() == reflect.Slice { + // Create a copy of tov, so we don't modify the original. + c := reflect.MakeSlice(tov.Type(), tov.Len(), tov.Len()+len(from)) + reflect.Copy(c, tov) + tov = c + } + if tov.Kind() != reflect.Slice { return nil, fmt.Errorf("expected a slice, got %T", to) } tot = tov.Type().Elem() + if tot.Kind() == reflect.Slice { + totvt := tot.Elem() + fromvs := make([]reflect.Value, len(from)) + for i, f := range from { + fromv := reflect.ValueOf(f) + fromt := fromv.Type() + if fromt.Kind() == reflect.Slice { + fromt = fromt.Elem() + } + if totvt != fromt { + return nil, fmt.Errorf("cannot append slice of %s to slice of %s", fromt, totvt) + } else { + fromvs[i] = fromv + } + } + return reflect.Append(tov, fromvs...).Interface(), nil + + } + toIsNil = tov.Len() == 0 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() + } if fromv.Kind() == reflect.Slice { if toIsNil { // If we get nil []string, we just return the []string return from[0], nil } - fromt := reflect.TypeOf(from[0]).Elem() - // If we get []string []string, we append the from slice to to if tot == fromt { return reflect.AppendSlice(tov, fromv).Interface(), nil @@ -52,6 +87,7 @@ func Append(to any, from ...any) (any, error) { // Fall back to a []interface{} slice. return appendToInterfaceSliceFromValues(tov, fromv) } + } } } @@ -62,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...) @@ -77,7 +113,11 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro var tos []any for _, slice := range []reflect.Value{slice1, slice2} { - for i := 0; i < slice.Len(); i++ { + if !slice.IsValid() { + tos = append(tos, nil) + continue + } + for i := range slice.Len() { tos = append(tos, slice.Index(i).Interface()) } } @@ -88,7 +128,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) { var tos []any - for i := 0; i < tov.Len(); i++ { + for i := range tov.Len() { tos = append(tos, tov.Index(i).Interface()) } diff --git a/common/collections/append_test.go b/common/collections/append_test.go index 6df32fee6..62d9015ce 100644 --- a/common/collections/append_test.go +++ b/common/collections/append_test.go @@ -15,6 +15,7 @@ package collections import ( "html/template" + "reflect" "testing" qt "github.com/frankban/quicktest" @@ -24,7 +25,7 @@ func TestAppend(t *testing.T) { t.Parallel() c := qt.New(t) - for _, test := range []struct { + for i, test := range []struct { start any addend []any expected any @@ -55,7 +56,7 @@ func TestAppend(t *testing.T) { []any{&tstSlicerIn1{"c"}}, testSlicerInterfaces{&tstSlicerIn1{"a"}, &tstSlicerIn1{"b"}, &tstSlicerIn1{"c"}}, }, - //https://github.com/gohugoio/hugo/issues/5361 + // https://github.com/gohugoio/hugo/issues/5361 { []string{"a", "b"}, []any{tstSlicers{&tstSlicer{"a"}, &tstSlicer{"b"}}}, @@ -74,6 +75,10 @@ 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"}}, + {[]string{"a", "b"}, []any{}, []string{"a", "b"}}, } { result, err := Append(test.start, test.addend...) @@ -85,6 +90,124 @@ func TestAppend(t *testing.T) { } c.Assert(err, qt.IsNil) - c.Assert(result, qt.DeepEquals, test.expected) + c.Assert(result, qt.DeepEquals, test.expected, qt.Commentf("test: [%d] %v", i, test)) + } +} + +// #11093 +func TestAppendToMultiDimensionalSlice(t *testing.T) { + t.Parallel() + c := qt.New(t) + + for _, test := range []struct { + to any + from []any + expected any + }{ + { + [][]string{{"a", "b"}}, + []any{[]string{"c", "d"}}, + [][]string{ + {"a", "b"}, + {"c", "d"}, + }, + }, + { + [][]string{{"a", "b"}}, + []any{[]string{"c", "d"}, []string{"e", "f"}}, + [][]string{ + {"a", "b"}, + {"c", "d"}, + {"e", "f"}, + }, + }, + { + [][]string{{"a", "b"}}, + []any{[]int{1, 2}}, + false, + }, + } { + result, err := Append(test.to, test.from...) + if b, ok := test.expected.(bool); ok && !b { + c.Assert(err, qt.Not(qt.IsNil)) + } else { + c.Assert(err, qt.IsNil) + c.Assert(result, qt.DeepEquals, test.expected) + } + } +} + +func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) { + t.Parallel() + c := qt.New(t) + slice := make([]string, 0, 100) + slice = append(slice, "a", "b") + result, err := Append(slice, "c") + c.Assert(err, qt.IsNil) + slice[0] = "d" + c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(slice, qt.DeepEquals, []string{"d", "b"}) +} + +func TestIndirect(t *testing.T) { + t.Parallel() + c := qt.New(t) + + type testStruct struct { + Field string + } + + var ( + nilPtr *testStruct + nilIface interface{} = nil + nonNilIface interface{} = &testStruct{Field: "hello"} + ) + + tests := []struct { + name string + input any + wantKind reflect.Kind + wantNil bool + }{ + { + name: "nil pointer", + input: nilPtr, + wantKind: reflect.Ptr, + wantNil: true, + }, + { + name: "nil interface", + input: nilIface, + wantKind: reflect.Invalid, + wantNil: false, + }, + { + name: "non-nil pointer to struct", + input: &testStruct{Field: "abc"}, + wantKind: reflect.Struct, + wantNil: false, + }, + { + name: "non-nil interface holding pointer", + input: nonNilIface, + wantKind: reflect.Struct, + wantNil: false, + }, + { + name: "plain value", + input: testStruct{Field: "xyz"}, + wantKind: reflect.Struct, + wantNil: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := reflect.ValueOf(tt.input) + got, isNil := indirect(v) + + c.Assert(got.Kind(), qt.Equals, tt.wantKind) + c.Assert(isNil, qt.Equals, tt.wantNil) + }) } } diff --git a/common/collections/slice.go b/common/collections/slice.go index bf5c7b52b..731f489f9 100644 --- a/common/collections/slice.go +++ b/common/collections/slice.go @@ -73,7 +73,6 @@ func StringSliceToInterfaceSlice(ss []string) []any { result[i] = s } return result - } type SortedStringSlice []string diff --git a/common/collections/slice_test.go b/common/collections/slice_test.go index 5788b9161..4008a5e6c 100644 --- a/common/collections/slice_test.go +++ b/common/collections/slice_test.go @@ -135,5 +135,38 @@ func TestSortedStringSlice(t *testing.T) { c.Assert(s.Count("b"), qt.Equals, 3) c.Assert(s.Count("z"), qt.Equals, 0) c.Assert(s.Count("a"), qt.Equals, 1) - +} + +func TestStringSliceToInterfaceSlice(t *testing.T) { + t.Parallel() + c := qt.New(t) + + tests := []struct { + name string + in []string + want []any + }{ + { + name: "empty slice", + in: []string{}, + want: []any{}, + }, + { + name: "single element", + in: []string{"hello"}, + want: []any{"hello"}, + }, + { + name: "multiple elements", + in: []string{"a", "b", "c"}, + want: []any{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := StringSliceToInterfaceSlice(tt.in) + c.Assert(got, qt.DeepEquals, tt.want) + }) + } } diff --git a/common/collections/stack.go b/common/collections/stack.go new file mode 100644 index 000000000..ff0db2f02 --- /dev/null +++ b/common/collections/stack.go @@ -0,0 +1,82 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collections + +import "slices" + +import "sync" + +// Stack is a simple LIFO stack that is safe for concurrent use. +type Stack[T any] struct { + items []T + zero T + mu sync.RWMutex +} + +func NewStack[T any]() *Stack[T] { + return &Stack[T]{} +} + +func (s *Stack[T]) Push(item T) { + s.mu.Lock() + defer s.mu.Unlock() + s.items = append(s.items, item) +} + +func (s *Stack[T]) Pop() (T, bool) { + s.mu.Lock() + defer s.mu.Unlock() + if len(s.items) == 0 { + return s.zero, false + } + item := s.items[len(s.items)-1] + s.items = s.items[:len(s.items)-1] + return item, true +} + +func (s *Stack[T]) Peek() (T, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + if len(s.items) == 0 { + return s.zero, false + } + return s.items[len(s.items)-1], true +} + +func (s *Stack[T]) Len() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.items) +} + +func (s *Stack[T]) Drain() []T { + s.mu.Lock() + defer s.mu.Unlock() + items := s.items + s.items = nil + return items +} + +func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T { + s.mu.Lock() + defer s.mu.Unlock() + var items []T + for i := len(s.items) - 1; i >= 0; i-- { + if predicate(s.items[i]) { + items = append(items, s.items[i]) + s.items = slices.Delete(s.items, i, i+1) + } + } + return items +} diff --git a/common/collections/stack_test.go b/common/collections/stack_test.go new file mode 100644 index 000000000..965d4dbc8 --- /dev/null +++ b/common/collections/stack_test.go @@ -0,0 +1,77 @@ +package collections + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestNewStack(t *testing.T) { + t.Parallel() + c := qt.New(t) + + s := NewStack[int]() + + c.Assert(s, qt.IsNotNil) +} + +func TestStackBasic(t *testing.T) { + t.Parallel() + c := qt.New(t) + + s := NewStack[int]() + + c.Assert(s.Len(), qt.Equals, 0) + + s.Push(1) + s.Push(2) + s.Push(3) + + c.Assert(s.Len(), qt.Equals, 3) + + top, ok := s.Peek() + c.Assert(ok, qt.Equals, true) + c.Assert(top, qt.Equals, 3) + + popped, ok := s.Pop() + c.Assert(ok, qt.Equals, true) + c.Assert(popped, qt.Equals, 3) + + c.Assert(s.Len(), qt.Equals, 2) + + _, _ = s.Pop() + _, _ = s.Pop() + _, ok = s.Pop() + + c.Assert(ok, qt.Equals, false) +} + +func TestStackDrain(t *testing.T) { + t.Parallel() + c := qt.New(t) + + s := NewStack[string]() + s.Push("a") + s.Push("b") + + got := s.Drain() + + c.Assert(got, qt.DeepEquals, []string{"a", "b"}) + c.Assert(s.Len(), qt.Equals, 0) +} + +func TestStackDrainMatching(t *testing.T) { + t.Parallel() + c := qt.New(t) + + s := NewStack[int]() + s.Push(1) + s.Push(2) + s.Push(3) + s.Push(4) + + got := s.DrainMatching(func(v int) bool { return v%2 == 0 }) + + c.Assert(got, qt.DeepEquals, []int{4, 2}) + c.Assert(s.Drain(), qt.DeepEquals, []int{1, 3}) +} diff --git a/common/constants/constants.go b/common/constants/constants.go index e416d4ad3..c7bbaa541 100644 --- a/common/constants/constants.go +++ b/common/constants/constants.go @@ -13,13 +13,37 @@ package constants -// Error IDs. +// Error/Warning IDs. // Do not change these values. const ( - ErrIDAmbigousDisableKindTaxonomy = "error-disable-taxonomy" - ErrIDAmbigousOutputKindTaxonomy = "error-output-taxonomy" - // IDs for remote errors in tpl/data. ErrRemoteGetJSON = "error-remote-getjson" ErrRemoteGetCSV = "error-remote-getcsv" + + WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides" + WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html" + WarnGoldmarkRawHTML = "warning-goldmark-raw-html" + WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix" + WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle" ) + +// Field/method names with special meaning. +const ( + FieldRelPermalink = "RelPermalink" + FieldPermalink = "Permalink" +) + +// IsFieldRelOrPermalink returns whether the given name is a RelPermalink or Permalink. +func IsFieldRelOrPermalink(name string) bool { + return name == FieldRelPermalink || name == FieldPermalink +} + +// Resource transformations. +const ( + ResourceTransformationFingerprint = "fingerprint" +) + +// IsResourceTransformationPermalinkHash returns whether the given name is a resource transformation that changes the permalink based on the content. +func IsResourceTransformationPermalinkHash(name string) bool { + return name == ResourceTransformationFingerprint +} diff --git a/common/hashing/hashing.go b/common/hashing/hashing.go new file mode 100644 index 000000000..e45356758 --- /dev/null +++ b/common/hashing/hashing.go @@ -0,0 +1,194 @@ +// Copyright 2024 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 hashing provides common hashing utilities. +package hashing + +import ( + "crypto/md5" + "encoding/hex" + "io" + "strconv" + "sync" + + "github.com/cespare/xxhash/v2" + "github.com/gohugoio/hashstructure" + "github.com/gohugoio/hugo/identity" +) + +// XXHashFromReader calculates the xxHash for the given reader. +func XXHashFromReader(r io.Reader) (uint64, int64, error) { + h := getXxHashReadFrom() + defer putXxHashReadFrom(h) + + size, err := io.Copy(h, r) + if err != nil { + return 0, 0, err + } + return h.Sum64(), size, nil +} + +// XxHashFromReaderHexEncoded calculates the xxHash for the given reader +// and returns the hash as a hex encoded string. +func XxHashFromReaderHexEncoded(r io.Reader) (string, error) { + h := getXxHashReadFrom() + defer putXxHashReadFrom(h) + _, err := io.Copy(h, r) + if err != nil { + return "", err + } + hash := h.Sum(nil) + return hex.EncodeToString(hash), nil +} + +// XXHashFromString calculates the xxHash for the given string. +func XXHashFromString(s string) (uint64, error) { + h := xxhash.New() + h.WriteString(s) + return h.Sum64(), nil +} + +// XxHashFromStringHexEncoded calculates the xxHash for the given string +// and returns the hash as a hex encoded string. +func XxHashFromStringHexEncoded(f string) string { + h := xxhash.New() + h.WriteString(f) + hash := h.Sum(nil) + return hex.EncodeToString(hash) +} + +// MD5FromStringHexEncoded returns the MD5 hash of the given string. +func MD5FromStringHexEncoded(f string) string { + h := md5.New() + h.Write([]byte(f)) + return hex.EncodeToString(h.Sum(nil)) +} + +// HashString returns a hash from the given elements. +// It will panic if the hash cannot be calculated. +// Note that this hash should be used primarily for identity, not for change detection as +// it in the more complex values (e.g. Page) will not hash the full content. +func HashString(vs ...any) string { + hash := HashUint64(vs...) + return strconv.FormatUint(hash, 10) +} + +// HashStringHex returns a hash from the given elements as a hex encoded string. +// See HashString for more information. +func HashStringHex(vs ...any) string { + hash := HashUint64(vs...) + return strconv.FormatUint(hash, 16) +} + +var hashOptsPool = sync.Pool{ + New: func() any { + return &hashstructure.HashOptions{ + Hasher: xxhash.New(), + } + }, +} + +func getHashOpts() *hashstructure.HashOptions { + return hashOptsPool.Get().(*hashstructure.HashOptions) +} + +func putHashOpts(opts *hashstructure.HashOptions) { + opts.Hasher.Reset() + hashOptsPool.Put(opts) +} + +// HashUint64 returns a hash from the given elements. +// It will panic if the hash cannot be calculated. +// Note that this hash should be used primarily for identity, not for change detection as +// it in the more complex values (e.g. Page) will not hash the full content. +func HashUint64(vs ...any) uint64 { + var o any + if len(vs) == 1 { + o = toHashable(vs[0]) + } else { + elements := make([]any, len(vs)) + for i, e := range vs { + elements[i] = toHashable(e) + } + o = elements + } + + hash, err := Hash(o) + if err != nil { + panic(err) + } + return hash +} + +// Hash returns a hash from vs. +func Hash(vs ...any) (uint64, error) { + hashOpts := getHashOpts() + defer putHashOpts(hashOpts) + var v any = vs + if len(vs) == 1 { + v = vs[0] + } + return hashstructure.Hash(v, hashOpts) +} + +type keyer interface { + Key() string +} + +// For structs, hashstructure.Hash only works on the exported fields, +// so rewrite the input slice for known identity types. +func toHashable(v any) any { + switch t := v.(type) { + case keyer: + return t.Key() + case identity.IdentityProvider: + return t.GetIdentity() + default: + return v + } +} + +type xxhashReadFrom struct { + buff []byte + *xxhash.Digest +} + +func (x *xxhashReadFrom) ReadFrom(r io.Reader) (int64, error) { + for { + n, err := r.Read(x.buff) + if n > 0 { + x.Digest.Write(x.buff[:n]) + } + if err != nil { + if err == io.EOF { + err = nil + } + return int64(n), err + } + } +} + +var xXhashReadFromPool = sync.Pool{ + New: func() any { + return &xxhashReadFrom{Digest: xxhash.New(), buff: make([]byte, 48*1024)} + }, +} + +func getXxHashReadFrom() *xxhashReadFrom { + return xXhashReadFromPool.Get().(*xxhashReadFrom) +} + +func putXxHashReadFrom(h *xxhashReadFrom) { + h.Reset() + xXhashReadFromPool.Put(h) +} diff --git a/common/hashing/hashing_test.go b/common/hashing/hashing_test.go new file mode 100644 index 000000000..105b6d8b5 --- /dev/null +++ b/common/hashing/hashing_test.go @@ -0,0 +1,157 @@ +// Copyright 2024 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 hashing + +import ( + "fmt" + "math" + "strings" + "sync" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestXxHashFromReader(t *testing.T) { + c := qt.New(t) + s := "Hello World" + r := strings.NewReader(s) + got, size, err := XXHashFromReader(r) + c.Assert(err, qt.IsNil) + c.Assert(size, qt.Equals, int64(len(s))) + c.Assert(got, qt.Equals, uint64(7148569436472236994)) +} + +func TestXxHashFromReaderPara(t *testing.T) { + c := qt.New(t) + + var wg sync.WaitGroup + for i := range 10 { + i := i + wg.Add(1) + go func() { + defer wg.Done() + for j := range 100 { + s := strings.Repeat("Hello ", i+j+1*42) + r := strings.NewReader(s) + got, size, err := XXHashFromReader(r) + c.Assert(size, qt.Equals, int64(len(s))) + c.Assert(err, qt.IsNil) + expect, _ := XXHashFromString(s) + c.Assert(got, qt.Equals, expect) + } + }() + } + + wg.Wait() +} + +func TestXxHashFromString(t *testing.T) { + c := qt.New(t) + s := "Hello World" + got, err := XXHashFromString(s) + c.Assert(err, qt.IsNil) + c.Assert(got, qt.Equals, uint64(7148569436472236994)) +} + +func TestXxHashFromStringHexEncoded(t *testing.T) { + c := qt.New(t) + s := "The quick brown fox jumps over the lazy dog" + got := XxHashFromStringHexEncoded(s) + // Facit: https://asecuritysite.com/encryption/xxhash?val=The%20quick%20brown%20fox%20jumps%20over%20the%20lazy%20dog + c.Assert(got, qt.Equals, "0b242d361fda71bc") +} + +func BenchmarkXXHashFromReader(b *testing.B) { + r := strings.NewReader("Hello World") + b.ResetTimer() + for i := 0; i < b.N; i++ { + XXHashFromReader(r) + r.Seek(0, 0) + } +} + +func BenchmarkXXHashFromString(b *testing.B) { + s := "Hello World" + b.ResetTimer() + for i := 0; i < b.N; i++ { + XXHashFromString(s) + } +} + +func BenchmarkXXHashFromStringHexEncoded(b *testing.B) { + s := "The quick brown fox jumps over the lazy dog" + b.ResetTimer() + for i := 0; i < b.N; i++ { + XxHashFromStringHexEncoded(s) + } +} + +func TestHashString(t *testing.T) { + c := qt.New(t) + + c.Assert(HashString("a", "b"), qt.Equals, "3176555414984061461") + c.Assert(HashString("ab"), qt.Equals, "7347350983217793633") + + var vals []any = []any{"a", "b", tstKeyer{"c"}} + + c.Assert(HashString(vals...), qt.Equals, "4438730547989914315") + c.Assert(vals[2], qt.Equals, tstKeyer{"c"}) +} + +type tstKeyer struct { + key string +} + +func (t tstKeyer) Key() string { + return t.key +} + +func (t tstKeyer) String() string { + return "key: " + t.key +} + +func BenchmarkHashString(b *testing.B) { + word := " hello " + + var tests []string + + for i := 1; i <= 5; i++ { + sentence := strings.Repeat(word, int(math.Pow(4, float64(i)))) + tests = append(tests, sentence) + } + + b.ResetTimer() + + for _, test := range tests { + b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) { + for i := 0; i < b.N; i++ { + HashString(test) + } + }) + } +} + +func BenchmarkHashMap(b *testing.B) { + m := map[string]any{} + for i := range 1000 { + m[fmt.Sprintf("key%d", i)] = i + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + HashString(m) + } +} diff --git a/common/hcontext/context.go b/common/hcontext/context.go new file mode 100644 index 000000000..9524ef284 --- /dev/null +++ b/common/hcontext/context.go @@ -0,0 +1,46 @@ +// Copyright 2024 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 hcontext + +import "context" + +// ContextDispatcher is a generic interface for setting and getting values from a context. +type ContextDispatcher[T any] interface { + Set(ctx context.Context, value T) context.Context + Get(ctx context.Context) T +} + +// NewContextDispatcher creates a new ContextDispatcher with the given key. +func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] { + return keyInContext[T, R]{ + id: key, + } +} + +type keyInContext[T any, R comparable] struct { + zero T + id R +} + +func (f keyInContext[T, R]) Get(ctx context.Context) T { + v := ctx.Value(f.id) + if v == nil { + return f.zero + } + return v.(T) +} + +func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context { + return context.WithValue(ctx, f.id, value) +} diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index 7624bab98..acaebb4bc 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -33,7 +33,7 @@ type LineMatcher struct { } // LineMatcherFn is used to match a line with an error. -// It returns the column number or 0 if the line was found, but column could not be determinde. Returns -1 if no line match. +// It returns the column number or 0 if the line was found, but column could not be determined. Returns -1 if no line match. type LineMatcherFn func(m LineMatcher) int // SimpleLineMatcher simply matches by line number. @@ -74,7 +74,6 @@ func ContainsMatcher(text string) func(m LineMatcher) int { // ErrorContext contains contextual information about an error. This will // typically be the lines surrounding some problem in a file. type ErrorContext struct { - // If a match will contain the matched line and up to 2 lines before and after. // Will be empty if no match. Lines []string @@ -153,10 +152,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext } if ectx.Position.LineNumber > 0 { - low := ectx.Position.LineNumber - 3 - if low < 0 { - low = 0 - } + low := max(ectx.Position.LineNumber-3, 0) if ectx.Position.LineNumber > 2 { ectx.LinesPos = 2 @@ -164,10 +160,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext ectx.LinesPos = ectx.Position.LineNumber - 1 } - high := ectx.Position.LineNumber + 2 - if high > len(lines) { - high = len(lines) - } + high := min(ectx.Position.LineNumber+2, len(lines)) ectx.Lines = lines[low:high] diff --git a/common/herrors/error_locator_test.go b/common/herrors/error_locator_test.go index 6135657d8..62f15213d 100644 --- a/common/herrors/error_locator_test.go +++ b/common/herrors/error_locator_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. diff --git a/common/herrors/errors.go b/common/herrors/errors.go index 4d8642362..c7ee90dd0 100644 --- a/common/herrors/errors.go +++ b/common/herrors/errors.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -15,14 +15,15 @@ package herrors import ( - "bytes" "errors" "fmt" "io" "os" + "regexp" "runtime" "runtime/debug" - "strconv" + "strings" + "time" ) // PrintStackTrace prints the current stacktrace to w. @@ -49,21 +50,66 @@ func Recover(args ...any) { } } -// GetGID the current goroutine id. Used only for debugging. -func GetGID() uint64 { - b := make([]byte, 64) - b = b[:runtime.Stack(b, false)] - b = bytes.TrimPrefix(b, []byte("goroutine ")) - b = b[:bytes.IndexByte(b, ' ')] - n, _ := strconv.ParseUint(string(b), 10, 64) - return n +// IsTimeoutError returns true if the given error is or contains a TimeoutError. +func IsTimeoutError(err error) bool { + return errors.Is(err, &TimeoutError{}) +} + +type TimeoutError struct { + Duration time.Duration +} + +func (e *TimeoutError) Error() string { + return fmt.Sprintf("timeout after %s", e.Duration) +} + +func (e *TimeoutError) Is(target error) bool { + _, ok := target.(*TimeoutError) + return ok +} + +// errMessage wraps an error with a message. +type errMessage struct { + msg string + err error +} + +func (e *errMessage) Error() string { + return e.msg +} + +func (e *errMessage) Unwrap() error { + return e.err +} + +// 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) { @@ -86,3 +132,56 @@ func IsNotExist(err error) bool { return false } + +// IsExist returns true if the error is a file exists error. +// Unlike os.IsExist, this also considers wrapped errors. +func IsExist(err error) bool { + if os.IsExist(err) { + return true + } + + // os.IsExist does not consider wrapped errors. + if os.IsExist(errors.Unwrap(err)) { + return true + } + + return false +} + +var nilPointerErrRe = regexp.MustCompile(`at <(.*)>: error calling (.*?): runtime error: invalid memory address or nil pointer dereference`) + +const deferredPrefix = "__hdeferred/" + +var deferredStringToRemove = regexp.MustCompile(`executing "__hdeferred/.*?" `) + +// ImproveRenderErr improves the error message for rendering errors. +func ImproveRenderErr(inErr error) (outErr error) { + outErr = inErr + msg := improveIfNilPointerMsg(inErr) + if msg != "" { + outErr = &errMessage{msg: msg, err: outErr} + } + + if strings.Contains(inErr.Error(), deferredPrefix) { + msg := deferredStringToRemove.ReplaceAllString(inErr.Error(), "executing ") + outErr = &errMessage{msg: msg, err: outErr} + } + return +} + +func improveIfNilPointerMsg(inErr error) string { + m := nilPointerErrRe.FindStringSubmatch(inErr.Error()) + if len(m) == 0 { + return "" + } + call := m[1] + field := m[2] + parts := strings.Split(call, ".") + if len(parts) < 2 { + return "" + } + receiverName := parts[len(parts)-2] + receiver := strings.Join(parts[:len(parts)-1], ".") + s := fmt.Sprintf("– %s is nil; wrap it in if or with: {{ with %s }}{{ .%s }}{{ end }}", receiverName, receiver, field) + return nilPointerErrRe.ReplaceAllString(inErr.Error(), s) +} diff --git a/common/herrors/errors_test.go b/common/herrors/errors_test.go index 1e0730028..2f53a1e89 100644 --- a/common/herrors/errors_test.go +++ b/common/herrors/errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -14,6 +14,7 @@ package herrors import ( + "errors" "fmt" "testing" @@ -34,3 +35,11 @@ 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) +} diff --git a/common/herrors/file_error.go b/common/herrors/file_error.go index 30417897f..38b198656 100644 --- a/common/herrors/file_error.go +++ b/common/herrors/file_error.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -15,19 +15,18 @@ package herrors import ( "encoding/json" + "errors" "fmt" "io" "path/filepath" - "github.com/bep/godartsass" + "github.com/bep/godartsass/v2" "github.com/bep/golibsass/libsass/libsasserrors" "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/text" "github.com/pelletier/go-toml/v2" "github.com/spf13/afero" "github.com/tdewolff/parse/v2" - - "errors" ) // FileError represents an error when handling a file: Parsing a config file, @@ -45,6 +44,9 @@ type FileError interface { // UpdateContent updates the error with a new ErrorContext from the content of the file. UpdateContent(r io.Reader, linematcher LineMatcherFn) FileError + + // SetFilename sets the filename of the error. + SetFilename(filename string) FileError } // Unwrapper can unwrap errors created with fmt.Errorf. @@ -57,6 +59,11 @@ var ( _ Unwrapper = (*fileError)(nil) ) +func (fe *fileError) SetFilename(filename string) FileError { + fe.position.Filename = filename + return fe +} + func (fe *fileError) UpdatePosition(pos text.Position) FileError { oldFilename := fe.Position().Filename if pos.Filename != "" && fe.fileType == "" { @@ -112,7 +119,6 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE } return fe - } type fileError struct { @@ -176,7 +182,6 @@ func NewFileErrorFromName(err error, name string) FileError { } return &fileError{cause: err, fileType: fileType, position: pos} - } // NewFileErrorFromPos will use the filename and line number from pos to create a new FileError, wrapping err. @@ -187,7 +192,6 @@ func NewFileErrorFromPos(err error, pos text.Position) FileError { _, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(pos.Filename)) } return &fileError{cause: err, fileType: fileType, position: pos} - } func NewFileErrorFromFileInErr(err error, fs afero.Fs, linematcher LineMatcherFn) FileError { @@ -244,7 +248,6 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) { }); ok { realFilename = s.Filename() } - } f, err2 := fs.Open(filename) @@ -255,8 +258,27 @@ func openFile(filename string, fs afero.Fs) (afero.File, string, error) { return f, realFilename, nil } -// Cause returns the underlying error or itself if it does not implement Unwrap. +// Cause returns the underlying error, that is, +// it unwraps errors until it finds one that does not implement +// the Unwrap method. +// For a shallow variant, see Unwrap. func Cause(err error) error { + type unwrapper interface { + Unwrap() error + } + + for err != nil { + cause, ok := err.(unwrapper) + if !ok { + break + } + err = cause.Unwrap() + } + return err +} + +// Unwrap returns the underlying error or itself if it does not implement Unwrap. +func Unwrap(err error) error { if u := errors.Unwrap(err); u != nil { return u } @@ -264,7 +286,7 @@ func Cause(err error) error { } func extractFileTypePos(err error) (string, text.Position) { - err = Cause(err) + err = Unwrap(err) var fileType string @@ -292,7 +314,7 @@ func extractFileTypePos(err error) (string, text.Position) { } // The error type from the minifier contains line number and column number. - if line, col := exctractLineNumberAndColumnNumber(err); line >= 0 { + if line, col := extractLineNumberAndColumnNumber(err); line >= 0 { pos.LineNumber = line pos.ColumnNumber = col return fileType, pos @@ -364,7 +386,7 @@ func extractOffsetAndType(e error) (int, string) { } } -func exctractLineNumberAndColumnNumber(e error) (int, int) { +func extractLineNumberAndColumnNumber(e error) (int, int) { switch v := e.(type) { case *parse.Error: return v.Line, v.Column @@ -381,7 +403,7 @@ func extractPosition(e error) (pos text.Position) { case godartsass.SassError: span := v.Span start := span.Start - filename, _ := paths.UrlToFilename(span.Url) + filename, _ := paths.UrlStringToFilename(span.Url) pos.Filename = filename pos.Offset = start.Offset pos.ColumnNumber = start.Column diff --git a/common/herrors/file_error_test.go b/common/herrors/file_error_test.go index 0b260a255..7aca08405 100644 --- a/common/herrors/file_error_test.go +++ b/common/herrors/file_error_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -14,12 +14,11 @@ package herrors import ( + "errors" "fmt" "strings" "testing" - "errors" - "github.com/gohugoio/hugo/common/text" qt "github.com/frankban/quicktest" @@ -48,7 +47,6 @@ func TestNewFileError(t *testing.T) { c.Assert(errorContext.Lines, qt.DeepEquals, []string{"line 30", "line 31", "line 32", "line 33", "line 34"}) c.Assert(errorContext.LinesPos, qt.Equals, 2) c.Assert(errorContext.ChromaLexer, qt.Equals, "go-html-template") - } func TestNewFileErrorExtractFromMessage(t *testing.T) { diff --git a/common/hexec/exec.go b/common/hexec/exec.go index 7a9fdd938..c3a6ebf57 100644 --- a/common/hexec/exec.go +++ b/common/hexec/exec.go @@ -19,13 +19,16 @@ import ( "errors" "fmt" "io" - "regexp" - "strings" - "os" "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" - "github.com/cli/safeexec" + "github.com/bep/logg" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/security" ) @@ -85,7 +88,7 @@ var WithEnviron = func(env []string) func(c *commandeer) { } // New creates a new Exec using the provided security config. -func New(cfg security.Config) *Exec { +func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec { var baseEnviron []string for _, v := range os.Environ() { k, _ := config.SplitEnvVar(v) @@ -95,8 +98,11 @@ func New(cfg security.Config) *Exec { } return &Exec{ - sc: cfg, - baseEnviron: baseEnviron, + sc: cfg, + workingDir: workingDir, + infol: log.InfoCommand("exec"), + baseEnviron: baseEnviron, + newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](), } } @@ -106,29 +112,27 @@ func IsNotFound(err error) bool { return errors.As(err, ¬FoundErr) } -// SafeCommand is a wrapper around os/exec Command which uses a LookPath -// implementation that does not search in current directory before looking in PATH. -// See https://github.com/cli/safeexec and the linked issues. -func SafeCommand(name string, arg ...string) (*exec.Cmd, error) { - bin, err := safeexec.LookPath(name) - if err != nil { - return nil, err - } - - return exec.Command(bin, arg...), nil -} - -// Exec encorces a security policy for commands run via os/exec. +// Exec enforces a security policy for commands run via os/exec. type Exec struct { - sc security.Config + sc security.Config + workingDir string + infol logg.LevelLogger // os.Environ filtered by the Exec.OsEnviron whitelist filter. baseEnviron []string + + newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)] + npxInit sync.Once + npxAvailable bool +} + +func (e *Exec) New(name string, arg ...any) (Runner, error) { + return e.new(name, "", arg...) } // New will fail if name is not allowed according to the configured security policy. // Else a configured Runner will be returned ready to be Run. -func (e *Exec) New(name string, arg ...any) (Runner, error) { +func (e *Exec) new(name string, fullyQualifiedName string, arg ...any) (Runner, error) { if err := e.sc.CheckAllowedExec(name); err != nil { return nil, err } @@ -137,18 +141,112 @@ func (e *Exec) New(name string, arg ...any) (Runner, error) { copy(env, e.baseEnviron) cm := &commandeer{ - name: name, - env: env, + name: name, + fullyQualifiedName: fullyQualifiedName, + env: env, } return cm.command(arg...) - } -// Npx is a convenience method to create a Runner running npx --no-install . +// 3. Fall back to the PATH. +// If name is "tailwindcss", we will try the PATH as the second option. func (e *Exec) Npx(name string, arg ...any) (Runner, error) { - arg = append(arg[:0], append([]any{"--no-install", name}, arg[0:]...)...) - return e.New("npx", arg...) + if err := e.sc.CheckAllowedExec(name); err != nil { + return nil, err + } + + newRunner, err := e.newNPXRunnerCache.GetOrCreate(name, func() (func(...any) (Runner, error), error) { + type tryFunc func() func(...any) (Runner, error) + tryFuncs := map[binaryLocation]tryFunc{ + binaryLocationNodeModules: func() func(...any) (Runner, error) { + nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name) + _, err := exec.LookPath(nodeBinFilename) + if err != nil { + return nil + } + return func(arg2 ...any) (Runner, error) { + return e.new(name, nodeBinFilename, arg2...) + } + }, + binaryLocationNpx: func() func(...any) (Runner, error) { + e.checkNpx() + if !e.npxAvailable { + return nil + } + return func(arg2 ...any) (Runner, error) { + return e.npx(name, arg2...) + } + }, + binaryLocationPath: func() func(...any) (Runner, error) { + if _, err := exec.LookPath(name); err != nil { + return nil + } + return func(arg2 ...any) (Runner, error) { + return e.New(name, arg2...) + } + }, + } + + locations := []binaryLocation{binaryLocationNodeModules, binaryLocationNpx, binaryLocationPath} + if name == "tailwindcss" { + // See https://github.com/gohugoio/hugo/issues/13221#issuecomment-2574801253 + locations = []binaryLocation{binaryLocationNodeModules, binaryLocationPath, binaryLocationNpx} + } + for _, loc := range locations { + if f := tryFuncs[loc](); f != nil { + e.infol.Logf("resolve %q using %s", name, loc) + return f, nil + } + } + return nil, &NotFoundError{name: name, method: fmt.Sprintf("in %s", locations[len(locations)-1])} + }) + if err != nil { + return nil, err + } + + return newRunner(arg...) +} + +const ( + npxNoInstall = "--no-install" + npxBinary = "npx" + nodeModulesBinPath = "node_modules/.bin" +) + +func (e *Exec) checkNpx() { + e.npxInit.Do(func() { + e.npxAvailable = InPath(npxBinary) + }) +} + +// npx is a convenience method to create a Runner running npx --no-install `, CurrentVersion.String())) } +// IsDevelopment reports whether the current running environment is "development". +func (i HugoInfo) IsDevelopment() bool { + return i.Environment == EnvironmentDevelopment +} + +// IsProduction reports whether the current running environment is "production". func (i HugoInfo) IsProduction() bool { return i.Environment == EnvironmentProduction } +// IsServer reports whether the built-in server is running. +func (i HugoInfo) IsServer() bool { + return i.conf.Running() +} + +// IsExtended reports whether the Hugo binary is the extended version. func (i HugoInfo) IsExtended() bool { return IsExtended } @@ -92,10 +119,57 @@ func (i HugoInfo) Deps() []*Dependency { return i.deps } +func (i HugoInfo) Store() *maps.Scratch { + return i.store +} + +// Deprecated: Use hugo.IsMultihost instead. +func (i HugoInfo) IsMultiHost() bool { + Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0") + return i.conf.IsMultihost() +} + +// IsMultihost reports whether each configured language has a unique baseURL. +func (i HugoInfo) IsMultihost() bool { + return i.conf.IsMultihost() +} + +// IsMultilingual reports whether there are two or more configured languages. +func (i HugoInfo) IsMultilingual() bool { + return i.conf.IsMultilingual() +} + +type contextKey uint8 + +const ( + contextKeyMarkupScope contextKey = iota +) + +var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope) + +type Context struct{} + +func (c Context) MarkupScope(ctx context.Context) string { + return GetMarkupScope(ctx) +} + +// SetMarkupScope sets the markup scope in the context. +func SetMarkupScope(ctx context.Context, s string) context.Context { + return markupScope.Set(ctx, s) +} + +// GetMarkupScope gets the markup scope from the context. +func GetMarkupScope(ctx context.Context) string { + return markupScope.Get(ctx) +} + // ConfigProvider represents the config options that are relevant for HugoInfo. type ConfigProvider interface { Environment() string + Running() bool WorkingDir() string + IsMultihost() bool + IsMultilingual() bool } // NewInfo creates a new Hugo Info object. @@ -122,6 +196,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo { Environment: conf.Environment(), conf: conf, deps: deps, + store: maps.NewScratch(), GoVersion: goVersion, } } @@ -141,7 +216,12 @@ func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []strin config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir)) if fs != nil { - fis, err := afero.ReadDir(fs, files.FolderJSConfig) + var fis []iofs.DirEntry + d, err := fs.Open(files.FolderJSConfig) + if err == nil { + fis, err = d.(iofs.ReadDirFile).ReadDir(-1) + } + if err == nil { for _, fi := range fis { key := fmt.Sprintf("HUGO_FILE_%s", strings.ReplaceAll(strings.ToUpper(fi.Name()), ".", "_")) @@ -166,8 +246,10 @@ type buildInfo struct { *debug.BuildInfo } -var bInfo *buildInfo -var bInfoInit sync.Once +var ( + bInfo *buildInfo + bInfoInit sync.Once +) func getBuildInfo() *buildInfo { bInfoInit.Do(func() { @@ -194,7 +276,6 @@ func getBuildInfo() *buildInfo { bInfo.GoArch = s.Value } } - }) return bInfo @@ -232,13 +313,16 @@ func GetDependencyListNonGo() []string { if IsExtended { deps = append( deps, - formatDep("github.com/sass/libsass", "3.6.5"), - formatDep("github.com/webmproject/libwebp", "v1.2.4"), + formatDep("github.com/sass/libsass", "3.6.6"), + formatDep("github.com/webmproject/libwebp", "v1.3.2"), ) } if dartSass := dartSassVersion(); dartSass.ProtocolVersion != "" { - const dartSassPath = "github.com/sass/dart-sass-embedded" + dartSassPath := "github.com/sass/dart-sass-embedded" + if IsDartSassGeV2() { + dartSassPath = "github.com/sass/dart-sass" + } deps = append(deps, formatDep(dartSassPath+"/protocol", dartSass.ProtocolVersion), formatDep(dartSassPath+"/compiler", dartSass.CompilerVersion), @@ -283,11 +367,101 @@ type Dependency struct { } func dartSassVersion() godartsass.DartSassVersion { - // This is also duplicated in the dartsass package. - const dartSassEmbeddedBinaryName = "dart-sass-embedded" - if !hexec.InPath(dartSassEmbeddedBinaryName) { + if DartSassBinaryName == "" || !IsDartSassGeV2() { return godartsass.DartSassVersion{} } - v, _ := godartsass.Version(dartSassEmbeddedBinaryName) + v, _ := godartsass.Version(DartSassBinaryName) return v } + +// DartSassBinaryName is the name of the Dart Sass binary to use. +// TODO(bep) find a better place for this. +var DartSassBinaryName string + +func init() { + DartSassBinaryName = os.Getenv("DART_SASS_BINARY") + if DartSassBinaryName == "" { + for _, name := range dartSassBinaryNamesV2 { + if hexec.InPath(name) { + DartSassBinaryName = name + break + } + } + if DartSassBinaryName == "" { + if hexec.InPath(dartSassBinaryNameV1) { + DartSassBinaryName = dartSassBinaryNameV1 + } + } + } +} + +var ( + dartSassBinaryNameV1 = "dart-sass-embedded" + dartSassBinaryNamesV2 = []string{"dart-sass", "sass"} +) + +// TODO(bep) we eventually want to remove this, but keep it for a while to throw an informative error. +// We stopped supporting the old binary in Hugo 0.139.0. +func IsDartSassGeV2() bool { + // dart-sass-embedded was the first version of the embedded Dart Sass before it was moved into the main project. + return !strings.Contains(DartSassBinaryName, "embedded") +} + +// Deprecate informs about a deprecation starting at the given version. +// +// A deprecation typically needs a simple change in the template, but doing so will make the template incompatible with older versions. +// Theme maintainers generally want +// 1. No warnings or errors in the console when building a Hugo site. +// 2. Their theme to work for at least the last few Hugo versions. +func Deprecate(item, alternative string, version string) { + level := deprecationLogLevelFromVersion(version) + deprecateLevel(item, alternative, version, level) +} + +// See Deprecate for details. +func DeprecateWithLogger(item, alternative string, version string, log logg.Logger) { + level := deprecationLogLevelFromVersion(version) + deprecateLevelWithLogger(item, alternative, version, level, log) +} + +// DeprecateLevelMin informs about a deprecation starting at the given version, but with a minimum log level. +func DeprecateLevelMin(item, alternative string, version string, minLevel logg.Level) { + level := max(deprecationLogLevelFromVersion(version), minLevel) + deprecateLevel(item, alternative, version, level) +} + +// deprecateLevel informs about a deprecation logging at the given level. +func deprecateLevel(item, alternative, version string, level logg.Level) { + deprecateLevelWithLogger(item, alternative, version, level, loggers.Log().Logger()) +} + +// DeprecateLevel informs about a deprecation logging at the given level. +func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) { + var msg string + if level == logg.LevelError { + msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative) + } else { + msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative) + } + + log.WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg) +} + +// We usually do about one minor version a month. +// We want people to run at least the current and previous version without any warnings. +// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature. +func deprecationLogLevelFromVersion(ver string) logg.Level { + from := MustParseVersion(ver) + to := CurrentVersion + minorDiff := to.Minor - from.Minor + switch { + case minorDiff >= 15: + // Start failing the build after about 15 months. + return logg.LevelError + case minorDiff >= 3: + // Start printing warnings after about 3 months. + return logg.LevelWarn + default: + return logg.LevelInfo + } +} diff --git a/common/hugo/hugo_integration_test.go b/common/hugo/hugo_integration_test.go new file mode 100644 index 000000000..77dbb5c91 --- /dev/null +++ b/common/hugo/hugo_integration_test.go @@ -0,0 +1,77 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugo_test + +import ( + "strings" + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestIsMultilingualAndIsMultihost(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +defaultContentLanguageInSubdir = true +[languages.de] +baseURL = 'https://de.example.org/' +[languages.en] +baseURL = 'https://en.example.org/' +-- content/_index.md -- +--- +title: home +--- +-- layouts/index.html -- +multilingual={{ hugo.IsMultilingual }} +multihost={{ hugo.IsMultihost }} + ` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/de/index.html", + "multilingual=true", + "multihost=true", + ) + b.AssertFileContent("public/en/index.html", + "multilingual=true", + "multihost=true", + ) + + files = strings.ReplaceAll(files, "baseURL = 'https://de.example.org/'", "") + files = strings.ReplaceAll(files, "baseURL = 'https://en.example.org/'", "") + + b = hugolib.Test(t, files) + + b.AssertFileContent("public/de/index.html", + "multilingual=true", + "multihost=false", + ) + b.AssertFileContent("public/en/index.html", + "multilingual=true", + "multihost=false", + ) + + files = strings.ReplaceAll(files, "[languages.de]", "") + files = strings.ReplaceAll(files, "[languages.en]", "") + + b = hugolib.Test(t, files) + + b.AssertFileContent("public/en/index.html", + "multilingual=false", + "multihost=false", + ) +} diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go index b0279f111..f938073da 100644 --- a/common/hugo/hugo_test.go +++ b/common/hugo/hugo_test.go @@ -14,16 +14,18 @@ package hugo import ( + "context" "fmt" "testing" + "github.com/bep/logg" qt "github.com/frankban/quicktest" ) func TestHugoInfo(t *testing.T) { c := qt.New(t) - conf := testConfig{environment: "production", workingDir: "/mywork"} + conf := testConfig{environment: "production", workingDir: "/mywork", running: false} hugoInfo := NewInfo(conf, nil) c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version()) @@ -38,22 +40,72 @@ func TestHugoInfo(t *testing.T) { } c.Assert(hugoInfo.Environment, qt.Equals, "production") c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version())) + c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false) c.Assert(hugoInfo.IsProduction(), qt.Equals, true) c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended) + c.Assert(hugoInfo.IsServer(), qt.Equals, false) - devHugoInfo := NewInfo(testConfig{environment: "development"}, nil) + devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil) + c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true) c.Assert(devHugoInfo.IsProduction(), qt.Equals, false) + c.Assert(devHugoInfo.IsServer(), qt.Equals, true) +} + +func TestDeprecationLogLevelFromVersion(t *testing.T) { + c := qt.New(t) + + c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError) + ver := CurrentVersion + c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo) + ver.Minor -= 3 + c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn) + ver.Minor -= 4 + c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn) + ver.Minor -= 13 + c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError) + + // Added just to find the threshold for where we can remove deprecated items. + // Subtract 5 from the minor version of the first ERRORed version => 0.122.0. + c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError) +} + +func TestMarkupScope(t *testing.T) { + c := qt.New(t) + + conf := testConfig{environment: "production", workingDir: "/mywork", running: false} + info := NewInfo(conf, nil) + + ctx := context.Background() + + ctx = SetMarkupScope(ctx, "foo") + + c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo") } type testConfig struct { - environment string - workingDir string + environment string + running bool + workingDir string + multihost bool + multilingual bool } func (c testConfig) Environment() string { return c.environment } +func (c testConfig) Running() bool { + return c.running +} + func (c testConfig) WorkingDir() string { return c.workingDir } + +func (c testConfig) IsMultihost() bool { + return c.multihost +} + +func (c testConfig) IsMultilingual() bool { + return c.multilingual +} diff --git a/common/hugo/vars_extended.go b/common/hugo/vars_extended.go index edbaff243..ab01e2647 100644 --- a/common/hugo/vars_extended.go +++ b/common/hugo/vars_extended.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build extended -// +build extended package hugo diff --git a/common/hugo/vars_regular.go b/common/hugo/vars_regular.go index 223df4b6c..a78aeb0b6 100644 --- a/common/hugo/vars_regular.go +++ b/common/hugo/vars_regular.go @@ -12,7 +12,6 @@ // limitations under the License. //go:build !extended -// +build !extended package hugo diff --git a/common/hugo/vars_withdeploy.go b/common/hugo/vars_withdeploy.go new file mode 100644 index 000000000..4e0c3efbb --- /dev/null +++ b/common/hugo/vars_withdeploy.go @@ -0,0 +1,18 @@ +// Copyright 2024 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. + +//go:build withdeploy + +package hugo + +var IsWithdeploy = true diff --git a/common/hugo/vars_withdeploy_off.go b/common/hugo/vars_withdeploy_off.go new file mode 100644 index 000000000..36e9bd874 --- /dev/null +++ b/common/hugo/vars_withdeploy_off.go @@ -0,0 +1,18 @@ +// Copyright 2024 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. + +//go:build !withdeploy + +package hugo + +var IsWithdeploy = false diff --git a/common/hugo/version.go b/common/hugo/version.go index 3bb6472e2..cf5988840 100644 --- a/common/hugo/version.go +++ b/common/hugo/version.go @@ -67,8 +67,11 @@ func (h VersionString) String() string { // Compare implements the compare.Comparer interface. func (h VersionString) Compare(other any) int { - v := MustParseVersion(h.String()) - return compareVersions(v, other) + return compareVersions(h.Version(), other) +} + +func (h VersionString) Version() Version { + return MustParseVersion(h.String()) } // Eq implements the compare.Eqer interface. @@ -149,6 +152,9 @@ func BuildVersionString() string { if IsExtended { version += "+extended" } + if IsWithdeploy { + version += "+withdeploy" + } osArch := bi.GoOS + "/" + bi.GoArch @@ -264,7 +270,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int { if v1maj > v2.Major { return 1 - } if v1maj < v2.Major { @@ -276,7 +281,6 @@ func compareFloatWithVersion(v1 float64, v2 Version) int { } return -1 - } func GoMinorVersion() int { diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go index b101e532b..ba367ceb5 100644 --- a/common/hugo/version_current.go +++ b/common/hugo/version_current.go @@ -17,7 +17,7 @@ package hugo // This should be the only one. var CurrentVersion = Version{ Major: 0, - Minor: 113, + Minor: 148, PatchLevel: 0, Suffix: "-DEV", } diff --git a/common/loggers/handlerdefault.go b/common/loggers/handlerdefault.go new file mode 100644 index 000000000..bc3c7eec2 --- /dev/null +++ b/common/loggers/handlerdefault.go @@ -0,0 +1,106 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers contains some basic logging setup. +package loggers + +import ( + "fmt" + "io" + "strings" + "sync" + + "github.com/bep/logg" + + "github.com/fatih/color" +) + +// levelColor mapping. +var levelColor = [...]*color.Color{ + logg.LevelTrace: color.New(color.FgWhite), + logg.LevelDebug: color.New(color.FgWhite), + logg.LevelInfo: color.New(color.FgBlue), + logg.LevelWarn: color.New(color.FgYellow), + logg.LevelError: color.New(color.FgRed), +} + +// levelString mapping. +var levelString = [...]string{ + logg.LevelTrace: "TRACE", + logg.LevelDebug: "DEBUG", + logg.LevelInfo: "INFO ", + logg.LevelWarn: "WARN ", + logg.LevelError: "ERROR", +} + +// newDefaultHandler handler. +func newDefaultHandler(outWriter, errWriter io.Writer) logg.Handler { + return &defaultHandler{ + outWriter: outWriter, + errWriter: errWriter, + Padding: 0, + } +} + +// Default Handler implementation. +// Based on https://github.com/apex/log/blob/master/handlers/cli/cli.go +type defaultHandler struct { + mu sync.Mutex + outWriter io.Writer // Defaults to os.Stdout. + errWriter io.Writer // Defaults to os.Stderr. + + Padding int +} + +// HandleLog implements logg.Handler. +func (h *defaultHandler) HandleLog(e *logg.Entry) error { + color := levelColor[e.Level] + level := levelString[e.Level] + + h.mu.Lock() + defer h.mu.Unlock() + + var w io.Writer + if e.Level > logg.LevelInfo { + w = h.errWriter + } else { + w = h.outWriter + } + + var prefix string + for _, field := range e.Fields { + if field.Name == FieldNameCmd { + prefix = fmt.Sprint(field.Value) + break + } + } + + if prefix != "" { + prefix = prefix + ": " + } + + color.Fprintf(w, "%s %s%s", fmt.Sprintf("%*s", h.Padding+1, level), color.Sprint(prefix), e.Message) + + for _, field := range e.Fields { + if strings.HasPrefix(field.Name, reservedFieldNamePrefix) { + continue + } + fmt.Fprintf(w, " %s %v", color.Sprint(field.Name), field.Value) + } + + fmt.Fprintln(w) + + return nil +} diff --git a/common/loggers/handlersmisc.go b/common/loggers/handlersmisc.go new file mode 100644 index 000000000..2ae6300f7 --- /dev/null +++ b/common/loggers/handlersmisc.go @@ -0,0 +1,145 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers + +import ( + "fmt" + "strings" + "sync" + + "github.com/bep/logg" + "github.com/gohugoio/hugo/common/hashing" +) + +// PanicOnWarningHook panics on warnings. +var PanicOnWarningHook = func(e *logg.Entry) error { + if e.Level != logg.LevelWarn { + return nil + } + panic(e.Message) +} + +func newLogLevelCounter() *logLevelCounter { + return &logLevelCounter{ + counters: make(map[logg.Level]int), + } +} + +func newLogOnceHandler(threshold logg.Level) *logOnceHandler { + return &logOnceHandler{ + threshold: threshold, + seen: make(map[uint64]bool), + } +} + +func newStopHandler(h ...logg.Handler) *stopHandler { + return &stopHandler{ + handlers: h, + } +} + +func newSuppressStatementsHandler(statements map[string]bool) *suppressStatementsHandler { + return &suppressStatementsHandler{ + statements: statements, + } +} + +type logLevelCounter struct { + mu sync.RWMutex + counters map[logg.Level]int +} + +func (h *logLevelCounter) HandleLog(e *logg.Entry) error { + h.mu.Lock() + defer h.mu.Unlock() + h.counters[e.Level]++ + return nil +} + +var errStop = fmt.Errorf("stop") + +type logOnceHandler struct { + threshold logg.Level + mu sync.Mutex + seen map[uint64]bool +} + +func (h *logOnceHandler) HandleLog(e *logg.Entry) error { + if e.Level < h.threshold { + // We typically only want to enable this for warnings and above. + // The common use case is that many go routines may log the same error. + return nil + } + h.mu.Lock() + defer h.mu.Unlock() + hash := hashing.HashUint64(e.Level, e.Message, e.Fields) + if h.seen[hash] { + return errStop + } + h.seen[hash] = true + return nil +} + +func (h *logOnceHandler) reset() { + h.mu.Lock() + defer h.mu.Unlock() + h.seen = make(map[uint64]bool) +} + +type stopHandler struct { + handlers []logg.Handler +} + +// HandleLog implements logg.Handler. +func (h *stopHandler) HandleLog(e *logg.Entry) error { + for _, handler := range h.handlers { + if err := handler.HandleLog(e); err != nil { + if err == errStop { + return nil + } + return err + } + } + return nil +} + +type suppressStatementsHandler struct { + statements map[string]bool +} + +func (h *suppressStatementsHandler) HandleLog(e *logg.Entry) error { + for _, field := range e.Fields { + if field.Name == FieldNameStatementID { + if h.statements[field.Value.(string)] { + return errStop + } + } + } + return nil +} + +// whiteSpaceTrimmer creates a new log handler that trims whitespace from log messages and string fields. +func whiteSpaceTrimmer() logg.Handler { + return logg.HandlerFunc(func(e *logg.Entry) error { + e.Message = strings.TrimSpace(e.Message) + for i, field := range e.Fields { + if s, ok := field.Value.(string); ok { + e.Fields[i].Value = strings.TrimSpace(s) + } + } + return nil + }) +} diff --git a/common/loggers/handlerterminal.go b/common/loggers/handlerterminal.go new file mode 100644 index 000000000..c6a86d3a2 --- /dev/null +++ b/common/loggers/handlerterminal.go @@ -0,0 +1,100 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers + +import ( + "fmt" + "io" + "regexp" + "strings" + "sync" + + "github.com/bep/logg" +) + +// newNoAnsiEscapeHandler creates a new noAnsiEscapeHandler +func newNoAnsiEscapeHandler(outWriter, errWriter io.Writer, noLevelPrefix bool, predicate func(*logg.Entry) bool) *noAnsiEscapeHandler { + if predicate == nil { + predicate = func(e *logg.Entry) bool { return true } + } + return &noAnsiEscapeHandler{ + noLevelPrefix: noLevelPrefix, + outWriter: outWriter, + errWriter: errWriter, + predicate: predicate, + } +} + +type noAnsiEscapeHandler struct { + mu sync.Mutex + outWriter io.Writer + errWriter io.Writer + predicate func(*logg.Entry) bool + noLevelPrefix bool +} + +func (h *noAnsiEscapeHandler) HandleLog(e *logg.Entry) error { + if !h.predicate(e) { + return nil + } + h.mu.Lock() + defer h.mu.Unlock() + + var w io.Writer + if e.Level > logg.LevelInfo { + w = h.errWriter + } else { + w = h.outWriter + } + + var prefix string + for _, field := range e.Fields { + if field.Name == FieldNameCmd { + prefix = fmt.Sprint(field.Value) + break + } + } + + if prefix != "" { + prefix = prefix + ": " + } + + msg := stripANSI(e.Message) + + if h.noLevelPrefix { + fmt.Fprintf(w, "%s%s", prefix, msg) + } else { + fmt.Fprintf(w, "%s %s%s", levelString[e.Level], prefix, msg) + } + + for _, field := range e.Fields { + if strings.HasPrefix(field.Name, reservedFieldNamePrefix) { + continue + } + fmt.Fprintf(w, " %s %v", field.Name, field.Value) + + } + fmt.Fprintln(w) + + return nil +} + +var ansiRe = regexp.MustCompile(`\x1b\[[0-9;]*m`) + +// stripANSI removes ANSI escape codes from s. +func stripANSI(s string) string { + return ansiRe.ReplaceAllString(s, "") +} diff --git a/common/loggers/handlerterminal_test.go b/common/loggers/handlerterminal_test.go new file mode 100644 index 000000000..f45ce80df --- /dev/null +++ b/common/loggers/handlerterminal_test.go @@ -0,0 +1,40 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers + +import ( + "bytes" + "testing" + + "github.com/bep/logg" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/terminal" +) + +func TestNoAnsiEscapeHandler(t *testing.T) { + c := qt.New(t) + + test := func(s string) { + c.Assert(stripANSI(terminal.Notice(s)), qt.Equals, s) + } + test(`error in "file.md:1:2"`) + + var buf bytes.Buffer + h := newNoAnsiEscapeHandler(&buf, &buf, false, nil) + h.HandleLog(&logg.Entry{Message: terminal.Notice(`error in "file.md:1:2"`), Level: logg.LevelInfo}) + + c.Assert(buf.String(), qt.Equals, "INFO error in \"file.md:1:2\"\n") +} diff --git a/common/loggers/ignorableLogger.go b/common/loggers/ignorableLogger.go deleted file mode 100644 index c8aba560e..000000000 --- a/common/loggers/ignorableLogger.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "fmt" -) - -// IgnorableLogger is a logger that ignores certain log statements. -type IgnorableLogger interface { - Logger - Errorsf(statementID, format string, v ...any) - Apply(logger Logger) IgnorableLogger -} - -type ignorableLogger struct { - Logger - statements map[string]bool -} - -// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given. -func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger { - if statements == nil { - statements = make(map[string]bool) - } - return ignorableLogger{ - Logger: logger, - statements: statements, - } -} - -// Errorsf logs statementID as an ERROR if not configured as ignoreable. -func (l ignorableLogger) Errorsf(statementID, format string, v ...any) { - if l.statements[statementID] { - // Ignore. - return - } - ignoreMsg := fmt.Sprintf(` -If you feel that this should not be logged as an ERROR, you can ignore it by adding this to your site config: -ignoreErrors = [%q]`, statementID) - - format += ignoreMsg - - l.Errorf(format, v...) -} - -func (l ignorableLogger) Apply(logger Logger) IgnorableLogger { - return ignorableLogger{ - Logger: logger, - statements: l.statements, - } -} diff --git a/common/loggers/logger.go b/common/loggers/logger.go new file mode 100644 index 000000000..a013049f7 --- /dev/null +++ b/common/loggers/logger.go @@ -0,0 +1,385 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers + +import ( + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/bep/logg" + "github.com/bep/logg/handlers/multi" + "github.com/gohugoio/hugo/common/terminal" +) + +var ( + reservedFieldNamePrefix = "__h_field_" + // FieldNameCmd is the name of the field that holds the command name. + FieldNameCmd = reservedFieldNamePrefix + "_cmd" + // Used to suppress statements. + FieldNameStatementID = reservedFieldNamePrefix + "__h_field_statement_id" +) + +// Options defines options for the logger. +type Options struct { + Level logg.Level + StdOut io.Writer + StdErr io.Writer + DistinctLevel logg.Level + StoreErrors bool + HandlerPost func(e *logg.Entry) error + SuppressStatements map[string]bool +} + +// New creates a new logger with the given options. +func New(opts Options) Logger { + if opts.StdOut == nil { + opts.StdOut = os.Stdout + } + if opts.StdErr == nil { + opts.StdErr = os.Stderr + } + + if opts.Level == 0 { + opts.Level = logg.LevelWarn + } + + var logHandler logg.Handler + if terminal.PrintANSIColors(os.Stderr) { + logHandler = newDefaultHandler(opts.StdErr, opts.StdErr) + } else { + logHandler = newNoAnsiEscapeHandler(opts.StdErr, opts.StdErr, false, nil) + } + + errorsw := &strings.Builder{} + logCounters := newLogLevelCounter() + handlers := []logg.Handler{ + logCounters, + } + + if opts.Level == logg.LevelTrace { + // Trace is used during development only, and it's useful to + // only see the trace messages. + handlers = append(handlers, + logg.HandlerFunc(func(e *logg.Entry) error { + if e.Level != logg.LevelTrace { + return logg.ErrStopLogEntry + } + return nil + }), + ) + } + + handlers = append(handlers, whiteSpaceTrimmer(), logHandler) + + if opts.HandlerPost != nil { + var hookHandler logg.HandlerFunc = func(e *logg.Entry) error { + opts.HandlerPost(e) + return nil + } + handlers = append(handlers, hookHandler) + } + + if opts.StoreErrors { + h := newNoAnsiEscapeHandler(io.Discard, errorsw, true, func(e *logg.Entry) bool { + return e.Level >= logg.LevelError + }) + + handlers = append(handlers, h) + } + + logHandler = multi.New(handlers...) + + var logOnce *logOnceHandler + if opts.DistinctLevel != 0 { + logOnce = newLogOnceHandler(opts.DistinctLevel) + logHandler = newStopHandler(logOnce, logHandler) + } + + if len(opts.SuppressStatements) > 0 { + logHandler = newStopHandler(newSuppressStatementsHandler(opts.SuppressStatements), logHandler) + } + + logger := logg.New( + logg.Options{ + Level: opts.Level, + Handler: logHandler, + }, + ) + + l := logger.WithLevel(opts.Level) + + reset := func() { + logCounters.mu.Lock() + defer logCounters.mu.Unlock() + logCounters.counters = make(map[logg.Level]int) + errorsw.Reset() + if logOnce != nil { + logOnce.reset() + } + } + + return &logAdapter{ + logCounters: logCounters, + errors: errorsw, + reset: reset, + stdOut: opts.StdOut, + stdErr: opts.StdErr, + level: opts.Level, + logger: logger, + tracel: l.WithLevel(logg.LevelTrace), + debugl: l.WithLevel(logg.LevelDebug), + infol: l.WithLevel(logg.LevelInfo), + warnl: l.WithLevel(logg.LevelWarn), + errorl: l.WithLevel(logg.LevelError), + } +} + +// NewDefault creates a new logger with the default options. +func NewDefault() Logger { + opts := Options{ + DistinctLevel: logg.LevelWarn, + Level: logg.LevelWarn, + } + return New(opts) +} + +func NewTrace() Logger { + opts := Options{ + DistinctLevel: logg.LevelWarn, + Level: logg.LevelTrace, + } + return New(opts) +} + +func LevelLoggerToWriter(l logg.LevelLogger) io.Writer { + return logWriter{l: l} +} + +type Logger interface { + Debug() logg.LevelLogger + Debugf(format string, v ...any) + Debugln(v ...any) + Error() logg.LevelLogger + Errorf(format string, v ...any) + Erroridf(id, format string, v ...any) + Errorln(v ...any) + Errors() string + Info() logg.LevelLogger + InfoCommand(command string) logg.LevelLogger + Infof(format string, v ...any) + Infoln(v ...any) + Level() logg.Level + LoggCount(logg.Level) int + Logger() logg.Logger + StdOut() io.Writer + StdErr() io.Writer + Printf(format string, v ...any) + Println(v ...any) + PrintTimerIfDelayed(start time.Time, name string) + Reset() + Warn() logg.LevelLogger + WarnCommand(command string) logg.LevelLogger + Warnf(format string, v ...any) + Warnidf(id, format string, v ...any) + Warnln(v ...any) + Deprecatef(fail bool, format string, v ...any) + Trace(s logg.StringFunc) +} + +type logAdapter struct { + logCounters *logLevelCounter + errors *strings.Builder + reset func() + stdOut io.Writer + stdErr io.Writer + level logg.Level + logger logg.Logger + tracel logg.LevelLogger + debugl logg.LevelLogger + infol logg.LevelLogger + warnl logg.LevelLogger + errorl logg.LevelLogger +} + +func (l *logAdapter) Debug() logg.LevelLogger { + return l.debugl +} + +func (l *logAdapter) Debugf(format string, v ...any) { + l.debugl.Logf(format, v...) +} + +func (l *logAdapter) Debugln(v ...any) { + l.debugl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Info() logg.LevelLogger { + return l.infol +} + +func (l *logAdapter) InfoCommand(command string) logg.LevelLogger { + return l.infol.WithField(FieldNameCmd, command) +} + +func (l *logAdapter) Infof(format string, v ...any) { + l.infol.Logf(format, v...) +} + +func (l *logAdapter) Infoln(v ...any) { + l.infol.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Level() logg.Level { + return l.level +} + +func (l *logAdapter) LoggCount(level logg.Level) int { + l.logCounters.mu.RLock() + defer l.logCounters.mu.RUnlock() + return l.logCounters.counters[level] +} + +func (l *logAdapter) Logger() logg.Logger { + return l.logger +} + +func (l *logAdapter) StdOut() io.Writer { + return l.stdOut +} + +func (l *logAdapter) StdErr() io.Writer { + return l.stdErr +} + +// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger +// if considerable time is spent. +func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) { + elapsed := time.Since(start) + milli := int(1000 * elapsed.Seconds()) + if milli < 500 { + return + } + fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli) +} + +func (l *logAdapter) Printf(format string, v ...any) { + // Add trailing newline if not present. + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + fmt.Fprintf(l.stdOut, format, v...) +} + +func (l *logAdapter) Println(v ...any) { + fmt.Fprintln(l.stdOut, v...) +} + +func (l *logAdapter) Reset() { + l.reset() +} + +func (l *logAdapter) Warn() logg.LevelLogger { + return l.warnl +} + +func (l *logAdapter) Warnf(format string, v ...any) { + l.warnl.Logf(format, v...) +} + +func (l *logAdapter) WarnCommand(command string) logg.LevelLogger { + return l.warnl.WithField(FieldNameCmd, command) +} + +func (l *logAdapter) Warnln(v ...any) { + l.warnl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Error() logg.LevelLogger { + return l.errorl +} + +func (l *logAdapter) Errorf(format string, v ...any) { + l.errorl.Logf(format, v...) +} + +func (l *logAdapter) Errorln(v ...any) { + l.errorl.Logf(l.sprint(v...)) +} + +func (l *logAdapter) Errors() string { + return l.errors.String() +} + +func (l *logAdapter) Erroridf(id, format string, v ...any) { + id = strings.ToLower(id) + format += l.idfInfoStatement("error", id, format) + l.errorl.WithField(FieldNameStatementID, id).Logf(format, v...) +} + +func (l *logAdapter) Warnidf(id, format string, v ...any) { + id = strings.ToLower(id) + format += l.idfInfoStatement("warning", id, format) + l.warnl.WithField(FieldNameStatementID, id).Logf(format, v...) +} + +func (l *logAdapter) idfInfoStatement(what, id, format string) string { + return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id) +} + +func (l *logAdapter) Trace(s logg.StringFunc) { + l.tracel.Log(s) +} + +func (l *logAdapter) sprint(v ...any) string { + return strings.TrimRight(fmt.Sprintln(v...), "\n") +} + +func (l *logAdapter) Deprecatef(fail bool, format string, v ...any) { + format = "DEPRECATED: " + format + if fail { + l.errorl.Logf(format, v...) + } else { + l.warnl.Logf(format, v...) + } +} + +type logWriter struct { + l logg.LevelLogger +} + +func (w logWriter) Write(p []byte) (n int, err error) { + w.l.Log(logg.String(string(p))) + return len(p), nil +} + +func TimeTrackf(l logg.LevelLogger, start time.Time, fields logg.Fields, format string, a ...any) { + elapsed := time.Since(start) + if fields != nil { + l = l.WithFields(fields) + } + l.WithField("duration", elapsed).Logf(format, a...) +} + +func TimeTrackfn(fn func() (logg.LevelLogger, error)) error { + start := time.Now() + l, err := fn() + elapsed := time.Since(start) + l.WithField("duration", elapsed).Logf("") + return err +} diff --git a/common/loggers/logger_test.go b/common/loggers/logger_test.go new file mode 100644 index 000000000..bc8975b06 --- /dev/null +++ b/common/loggers/logger_test.go @@ -0,0 +1,154 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers_test + +import ( + "io" + "strings" + "testing" + + "github.com/bep/logg" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/loggers" +) + +func TestLogDistinct(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + DistinctLevel: logg.LevelWarn, + StoreErrors: true, + StdOut: io.Discard, + StdErr: io.Discard, + } + + l := loggers.New(opts) + + for range 10 { + l.Errorln("error 1") + l.Errorln("error 2") + l.Warnln("warn 1") + } + c.Assert(strings.Count(l.Errors(), "error 1"), qt.Equals, 1) + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1) +} + +func TestHookLast(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + HandlerPost: func(e *logg.Entry) error { + panic(e.Message) + }, + StdOut: io.Discard, + StdErr: io.Discard, + } + + l := loggers.New(opts) + + c.Assert(func() { l.Warnln("warn 1") }, qt.PanicMatches, "warn 1") +} + +func TestOptionStoreErrors(t *testing.T) { + c := qt.New(t) + + var sb strings.Builder + + opts := loggers.Options{ + StoreErrors: true, + StdErr: &sb, + StdOut: &sb, + } + + l := loggers.New(opts) + l.Errorln("error 1") + l.Errorln("error 2") + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Contains, "error 1") + c.Assert(errorsStr, qt.Not(qt.Contains), "ERROR") + + c.Assert(sb.String(), qt.Contains, "error 1") + c.Assert(sb.String(), qt.Contains, "ERROR") +} + +func TestLogCount(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + } + + l := loggers.New(opts) + l.Errorln("error 1") + l.Errorln("error 2") + l.Warnln("warn 1") + + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + c.Assert(l.LoggCount(logg.LevelWarn), qt.Equals, 1) + c.Assert(l.LoggCount(logg.LevelInfo), qt.Equals, 0) +} + +func TestSuppressStatements(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + SuppressStatements: map[string]bool{ + "error-1": true, + }, + } + + l := loggers.New(opts) + l.Error().WithField(loggers.FieldNameStatementID, "error-1").Logf("error 1") + l.Errorln("error 2") + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Not(qt.Contains), "error 1") + c.Assert(errorsStr, qt.Contains, "error 2") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 1) +} + +func TestReset(t *testing.T) { + c := qt.New(t) + + opts := loggers.Options{ + StoreErrors: true, + DistinctLevel: logg.LevelWarn, + StdOut: io.Discard, + StdErr: io.Discard, + } + + l := loggers.New(opts) + + for range 3 { + l.Errorln("error 1") + l.Errorln("error 2") + l.Errorln("error 1") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 2) + + l.Reset() + + errorsStr := l.Errors() + + c.Assert(errorsStr, qt.Equals, "") + c.Assert(l.LoggCount(logg.LevelError), qt.Equals, 0) + + } +} diff --git a/common/loggers/loggerglobal.go b/common/loggers/loggerglobal.go new file mode 100644 index 000000000..b8c9a6931 --- /dev/null +++ b/common/loggers/loggerglobal.go @@ -0,0 +1,62 @@ +// Copyright 2024 The Hugo Authors. All rights reserved. +// Some functions in this file (see comments) is based on the Go source code, +// copyright The Go Authors and governed by a BSD-style license. +// +// 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 loggers + +import ( + "sync" + + "github.com/bep/logg" +) + +// SetGlobalLogger sets the global logger. +// This is used in a few places in Hugo, e.g. deprecated functions. +func SetGlobalLogger(logger Logger) { + logMu.Lock() + defer logMu.Unlock() + log = logger +} + +func initGlobalLogger(level logg.Level, panicOnWarnings bool) { + logMu.Lock() + defer logMu.Unlock() + var logHookLast func(e *logg.Entry) error + if panicOnWarnings { + logHookLast = PanicOnWarningHook + } + + log = New( + Options{ + Level: level, + DistinctLevel: logg.LevelInfo, + HandlerPost: logHookLast, + }, + ) +} + +var logMu sync.Mutex + +func Log() Logger { + logMu.Lock() + defer logMu.Unlock() + return log +} + +// The global logger. +var log Logger + +func init() { + initGlobalLogger(logg.LevelWarn, false) +} diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go deleted file mode 100644 index fbbbca435..000000000 --- a/common/loggers/loggers.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "regexp" - "runtime" - "sync/atomic" - "time" - - "github.com/gohugoio/hugo/common/terminal" - - jww "github.com/spf13/jwalterweatherman" -) - -var ( - // Counts ERROR logs to the global jww logger. - GlobalErrorCounter *jww.Counter - PanicOnWarning atomic.Bool -) - -func init() { - GlobalErrorCounter = &jww.Counter{} - jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError)) -} - -func LoggerToWriterWithPrefix(logger *log.Logger, prefix string) io.Writer { - return prefixWriter{ - logger: logger, - prefix: prefix, - } -} - -type prefixWriter struct { - logger *log.Logger - prefix string -} - -func (w prefixWriter) Write(p []byte) (n int, err error) { - w.logger.Printf("%s: %s", w.prefix, p) - return len(p), nil -} - -type Logger interface { - Printf(format string, v ...any) - Println(v ...any) - PrintTimerIfDelayed(start time.Time, name string) - Debug() *log.Logger - Debugf(format string, v ...any) - Debugln(v ...any) - Info() *log.Logger - Infof(format string, v ...any) - Infoln(v ...any) - Warn() *log.Logger - Warnf(format string, v ...any) - Warnln(v ...any) - Error() *log.Logger - Errorf(format string, v ...any) - Errorln(v ...any) - Errors() string - - Out() io.Writer - - Reset() - - // Used in tests. - LogCounters() *LogCounters -} - -type LogCounters struct { - ErrorCounter *jww.Counter - WarnCounter *jww.Counter -} - -type logger struct { - *jww.Notepad - - // The writer that represents stdout. - // Will be io.Discard when in quiet mode. - out io.Writer - - logCounters *LogCounters - - // This is only set in server mode. - errors *bytes.Buffer -} - -func (l *logger) Printf(format string, v ...any) { - l.FEEDBACK.Printf(format, v...) -} - -func (l *logger) Println(v ...any) { - l.FEEDBACK.Println(v...) -} - -func (l *logger) Debug() *log.Logger { - return l.DEBUG -} - -func (l *logger) Debugf(format string, v ...any) { - l.DEBUG.Printf(format, v...) -} - -func (l *logger) Debugln(v ...any) { - l.DEBUG.Println(v...) -} - -func (l *logger) Infof(format string, v ...any) { - l.INFO.Printf(format, v...) -} - -func (l *logger) Infoln(v ...any) { - l.INFO.Println(v...) -} - -func (l *logger) Info() *log.Logger { - return l.INFO -} - -const panicOnWarningMessage = "Warning trapped. Remove the --panicOnWarning flag to continue." - -func (l *logger) Warnf(format string, v ...any) { - l.WARN.Printf(format, v...) - if PanicOnWarning.Load() { - panic(panicOnWarningMessage) - } -} - -func (l *logger) Warnln(v ...any) { - l.WARN.Println(v...) - if PanicOnWarning.Load() { - panic(panicOnWarningMessage) - } -} - -func (l *logger) Warn() *log.Logger { - return l.WARN -} - -func (l *logger) Errorf(format string, v ...any) { - l.ERROR.Printf(format, v...) -} - -func (l *logger) Errorln(v ...any) { - l.ERROR.Println(v...) -} - -func (l *logger) Error() *log.Logger { - return l.ERROR -} - -func (l *logger) LogCounters() *LogCounters { - return l.logCounters -} - -func (l *logger) Out() io.Writer { - return l.out -} - -// PrintTimerIfDelayed prints a time statement to the FEEDBACK logger -// if considerable time is spent. -func (l *logger) PrintTimerIfDelayed(start time.Time, name string) { - elapsed := time.Since(start) - milli := int(1000 * elapsed.Seconds()) - if milli < 500 { - return - } - l.Printf("%s in %v ms", name, milli) -} - -func (l *logger) PrintTimer(start time.Time, name string) { - elapsed := time.Since(start) - milli := int(1000 * elapsed.Seconds()) - l.Printf("%s in %v ms", name, milli) -} - -func (l *logger) Errors() string { - if l.errors == nil { - return "" - } - return ansiColorRe.ReplaceAllString(l.errors.String(), "") -} - -// Reset resets the logger's internal state. -func (l *logger) Reset() { - l.logCounters.ErrorCounter.Reset() - if l.errors != nil { - l.errors.Reset() - } -} - -// NewLogger creates a new Logger for the given thresholds -func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) Logger { - return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors) -} - -// NewDebugLogger is a convenience function to create a debug logger. -func NewDebugLogger() Logger { - return NewBasicLogger(jww.LevelDebug) -} - -// NewWarningLogger is a convenience function to create a warning logger. -func NewWarningLogger() Logger { - return NewBasicLogger(jww.LevelWarn) -} - -// NewInfoLogger is a convenience function to create a info logger. -func NewInfoLogger() Logger { - return NewBasicLogger(jww.LevelInfo) -} - -// NewErrorLogger is a convenience function to create an error logger. -func NewErrorLogger() Logger { - return NewBasicLogger(jww.LevelError) -} - -// NewBasicLogger creates a new basic logger writing to Stdout. -func NewBasicLogger(t jww.Threshold) Logger { - return newLogger(t, jww.LevelError, os.Stdout, io.Discard, false) -} - -// NewBasicLoggerForWriter creates a new basic logger writing to w. -func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger { - return newLogger(t, jww.LevelError, w, io.Discard, false) -} - -// RemoveANSIColours removes all ANSI colours from the given string. -func RemoveANSIColours(s string) string { - return ansiColorRe.ReplaceAllString(s, "") -} - -var ( - ansiColorRe = regexp.MustCompile("(?s)\\033\\[\\d*(;\\d*)*m") - errorRe = regexp.MustCompile("^(ERROR|FATAL|WARN)") -) - -type ansiCleaner struct { - w io.Writer -} - -func (a ansiCleaner) Write(p []byte) (n int, err error) { - return a.w.Write(ansiColorRe.ReplaceAll(p, []byte(""))) -} - -type labelColorizer struct { - w io.Writer -} - -func (a labelColorizer) Write(p []byte) (n int, err error) { - replaced := errorRe.ReplaceAllStringFunc(string(p), func(m string) string { - switch m { - case "ERROR", "FATAL": - return terminal.Error(m) - case "WARN": - return terminal.Warning(m) - default: - return m - } - }) - // io.MultiWriter will abort if we return a bigger write count than input - // bytes, so we lie a little. - _, err = a.w.Write([]byte(replaced)) - return len(p), err -} - -// InitGlobalLogger initializes the global logger, used in some rare cases. -func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer) { - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - jww.SetStdoutOutput(outHandle) - jww.SetLogOutput(logHandle) - jww.SetLogThreshold(logThreshold) - jww.SetStdoutThreshold(stdoutThreshold) -} - -func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) { - isTerm := terminal.PrintANSIColors(os.Stdout) - if logHandle != io.Discard && isTerm { - // Remove any Ansi coloring from log output - logHandle = ansiCleaner{w: logHandle} - } - - if isTerm { - outHandle = labelColorizer{w: outHandle} - } - - return outHandle, logHandle -} - -type fatalLogWriter int - -func (s fatalLogWriter) Write(p []byte) (n int, err error) { - trace := make([]byte, 1500) - runtime.Stack(trace, true) - fmt.Printf("\n===========\n\n%s\n", trace) - os.Exit(-1) - - return 0, nil -} - -var fatalLogListener = func(t jww.Threshold) io.Writer { - if t != jww.LevelError { - // Only interested in ERROR - return nil - } - - return new(fatalLogWriter) -} - -func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *logger { - errorCounter := &jww.Counter{} - warnCounter := &jww.Counter{} - outHandle, logHandle = getLogWriters(outHandle, logHandle) - - listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError), jww.LogCounter(warnCounter, jww.LevelWarn)} - var errorBuff *bytes.Buffer - if saveErrors { - errorBuff = new(bytes.Buffer) - errorCapture := func(t jww.Threshold) io.Writer { - if t != jww.LevelError { - // Only interested in ERROR - return nil - } - return errorBuff - } - - listeners = append(listeners, errorCapture) - } - - return &logger{ - Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...), - out: outHandle, - logCounters: &LogCounters{ - ErrorCounter: errorCounter, - WarnCounter: warnCounter, - }, - errors: errorBuff, - } -} diff --git a/common/loggers/loggers_test.go b/common/loggers/loggers_test.go deleted file mode 100644 index a7bd1ae12..000000000 --- a/common/loggers/loggers_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2018 The Hugo Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package loggers - -import ( - "bytes" - "fmt" - "log" - "testing" - - qt "github.com/frankban/quicktest" -) - -func TestLogger(t *testing.T) { - c := qt.New(t) - l := NewWarningLogger() - - l.Errorln("One error") - l.Errorln("Two error") - l.Warnln("A warning") - - c.Assert(l.LogCounters().ErrorCounter.Count(), qt.Equals, uint64(2)) -} - -func TestLoggerToWriterWithPrefix(t *testing.T) { - c := qt.New(t) - - var b bytes.Buffer - - logger := log.New(&b, "", 0) - - w := LoggerToWriterWithPrefix(logger, "myprefix") - - fmt.Fprint(w, "Hello Hugo!") - - c.Assert(b.String(), qt.Equals, "myprefix: Hello Hugo!\n") -} - -func TestRemoveANSIColours(t *testing.T) { - c := qt.New(t) - - c.Assert(RemoveANSIColours(""), qt.Equals, "") - c.Assert(RemoveANSIColours("\033[31m"), qt.Equals, "") - c.Assert(RemoveANSIColours("\033[31mHello"), qt.Equals, "Hello") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m"), qt.Equals, "Hello") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m World"), qt.Equals, "Hello World") - c.Assert(RemoveANSIColours("\033[31mHello\033[0m World\033[31m!"), qt.Equals, "Hello World!") - c.Assert(RemoveANSIColours("\x1b[90m 5 |"), qt.Equals, " 5 |") -} diff --git a/common/maps/cache.go b/common/maps/cache.go new file mode 100644 index 000000000..de1535994 --- /dev/null +++ b/common/maps/cache.go @@ -0,0 +1,195 @@ +// Copyright 2024 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 maps + +import ( + "sync" +) + +// Cache is a simple thread safe cache backed by a map. +type Cache[K comparable, T any] struct { + m map[K]T + hasBeenInitialized bool + sync.RWMutex +} + +// NewCache creates a new Cache. +func NewCache[K comparable, T any]() *Cache[K, T] { + return &Cache[K, T]{m: make(map[K]T)} +} + +// Delete deletes the given key from the cache. +// If c is nil, this method is a no-op. +func (c *Cache[K, T]) Get(key K) (T, bool) { + if c == nil { + var zero T + return zero, false + } + c.RLock() + v, found := c.get(key) + c.RUnlock() + return v, found +} + +func (c *Cache[K, T]) get(key K) (T, bool) { + v, found := c.m[key] + return v, found +} + +// GetOrCreate gets the value for the given key if it exists, or creates it if not. +func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) { + c.RLock() + v, found := c.m[key] + c.RUnlock() + if found { + return v, nil + } + c.Lock() + defer c.Unlock() + v, found = c.m[key] + if found { + return v, nil + } + v, err := create() + if err != nil { + return v, err + } + c.m[key] = v + return v, nil +} + +// Contains returns whether the given key exists in the cache. +func (c *Cache[K, T]) Contains(key K) bool { + c.RLock() + _, found := c.m[key] + c.RUnlock() + return found +} + +// InitAndGet initializes the cache if not already done and returns the value for the given key. +// The init state will be reset on Reset or Drain. +func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) { + var v T + c.RLock() + if !c.hasBeenInitialized { + c.RUnlock() + if err := func() error { + c.Lock() + defer c.Unlock() + // Double check in case another goroutine has initialized it in the meantime. + if !c.hasBeenInitialized { + err := init(c.get, c.set) + if err != nil { + return err + } + c.hasBeenInitialized = true + } + return nil + }(); err != nil { + return v, err + } + // Reacquire the read lock. + c.RLock() + } + + v = c.m[key] + c.RUnlock() + + return v, nil +} + +// Set sets the given key to the given value. +func (c *Cache[K, T]) Set(key K, value T) { + c.Lock() + c.set(key, value) + c.Unlock() +} + +// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache. +func (c *Cache[K, T]) SetIfAbsent(key K, value T) { + c.RLock() + if _, found := c.get(key); !found { + c.RUnlock() + c.Set(key, value) + } else { + c.RUnlock() + } +} + +func (c *Cache[K, T]) set(key K, value T) { + c.m[key] = value +} + +// ForEeach calls the given function for each key/value pair in the cache. +// If the function returns false, the iteration stops. +func (c *Cache[K, T]) ForEeach(f func(K, T) bool) { + c.RLock() + defer c.RUnlock() + for k, v := range c.m { + if !f(k, v) { + return + } + } +} + +func (c *Cache[K, T]) Drain() map[K]T { + c.Lock() + m := c.m + c.m = make(map[K]T) + c.hasBeenInitialized = false + c.Unlock() + return m +} + +func (c *Cache[K, T]) Len() int { + c.RLock() + defer c.RUnlock() + return len(c.m) +} + +func (c *Cache[K, T]) Reset() { + c.Lock() + clear(c.m) + c.hasBeenInitialized = false + c.Unlock() +} + +// SliceCache is a simple thread safe cache backed by a map. +type SliceCache[T any] struct { + m map[string][]T + sync.RWMutex +} + +func NewSliceCache[T any]() *SliceCache[T] { + return &SliceCache[T]{m: make(map[string][]T)} +} + +func (c *SliceCache[T]) Get(key string) ([]T, bool) { + c.RLock() + v, found := c.m[key] + c.RUnlock() + return v, found +} + +func (c *SliceCache[T]) Append(key string, values ...T) { + c.Lock() + c.m[key] = append(c.m[key], values...) + c.Unlock() +} + +func (c *SliceCache[T]) Reset() { + c.Lock() + c.m = make(map[string][]T) + c.Unlock() +} diff --git a/common/maps/maps.go b/common/maps/maps.go index 6aefde927..f9171ebf2 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -29,7 +29,7 @@ func ToStringMapE(in any) (map[string]any, error) { case Params: return vv, nil case map[string]string: - var m = map[string]any{} + m := map[string]any{} for k, v := range vv { m[k] = v } @@ -112,17 +112,17 @@ func ToSliceStringMap(in any) ([]map[string]any, error) { } // LookupEqualFold finds key in m with case insensitive equality checks. -func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) { +func LookupEqualFold[T any | string](m map[string]T, key string) (T, string, bool) { if v, found := m[key]; found { - return v, true + return v, key, true } for k, v := range m { if strings.EqualFold(k, key) { - return v, true + return v, k, true } } var s T - return s, false + return s, "", false } // MergeShallow merges src into dst, but only if the key does not already exist in dst. @@ -192,21 +192,45 @@ func (KeyRenamer) keyPath(k1, k2 string) string { } func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]any) { - for key, val := range m { - keyPath := r.keyPath(parentKeyPath, key) - switch val.(type) { + for k, v := range m { + keyPath := r.keyPath(parentKeyPath, k) + switch vv := v.(type) { case map[any]any: - val = cast.ToStringMap(val) - r.renamePath(keyPath, val.(map[string]any)) + r.renamePath(keyPath, cast.ToStringMap(vv)) case map[string]any: - r.renamePath(keyPath, val.(map[string]any)) + r.renamePath(keyPath, vv) } newKey := r.getNewKey(keyPath) if newKey != "" { - delete(m, key) - m[newKey] = val + delete(m, k) + m[newKey] = v + } + } +} + +// ConvertFloat64WithNoDecimalsToInt converts float64 values with no decimals to int recursively. +func ConvertFloat64WithNoDecimalsToInt(m map[string]any) { + for k, v := range m { + switch vv := v.(type) { + case float64: + if v == float64(int64(vv)) { + m[k] = int64(vv) + } + case map[string]any: + ConvertFloat64WithNoDecimalsToInt(vv) + case []any: + for i, vvv := range vv { + switch vvvv := vvv.(type) { + case float64: + if vvv == float64(int64(vvvv)) { + vv[i] = int64(vvvv) + } + case map[string]any: + ConvertFloat64WithNoDecimalsToInt(vvvv) + } + } } } } diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go index 0e8589d34..40c8ac824 100644 --- a/common/maps/maps_test.go +++ b/common/maps/maps_test.go @@ -73,10 +73,14 @@ func TestPrepareParams(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { // PrepareParams modifies input. + prepareClone := PrepareParamsClone(test.input) PrepareParams(test.input) if !reflect.DeepEqual(test.expected, test.input) { t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) } + if !reflect.DeepEqual(test.expected, prepareClone) { + t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, prepareClone) + } }) } } @@ -180,17 +184,18 @@ func TestLookupEqualFold(t *testing.T) { "B": "bv", } - v, found := LookupEqualFold(m1, "b") + v, k, found := LookupEqualFold(m1, "b") c.Assert(found, qt.IsTrue) c.Assert(v, qt.Equals, "bv") + c.Assert(k, qt.Equals, "B") m2 := map[string]string{ "a": "av", "B": "bv", } - v, found = LookupEqualFold(m2, "b") + v, k, found = LookupEqualFold(m2, "b") c.Assert(found, qt.IsTrue) + c.Assert(k, qt.Equals, "B") c.Assert(v, qt.Equals, "bv") - } diff --git a/common/maps/ordered.go b/common/maps/ordered.go new file mode 100644 index 000000000..0da9d239d --- /dev/null +++ b/common/maps/ordered.go @@ -0,0 +1,144 @@ +// Copyright 2024 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 maps + +import ( + "slices" + + "github.com/gohugoio/hugo/common/hashing" +) + +// Ordered is a map that can be iterated in the order of insertion. +// Note that insertion order is not affected if a key is re-inserted into the map. +// In a nil map, all operations are no-ops. +// This is not thread safe. +type Ordered[K comparable, T any] struct { + // The keys in the order they were added. + keys []K + // The values. + values map[K]T +} + +// NewOrdered creates a new Ordered map. +func NewOrdered[K comparable, T any]() *Ordered[K, T] { + return &Ordered[K, T]{values: make(map[K]T)} +} + +// Set sets the value for the given key. +// Note that insertion order is not affected if a key is re-inserted into the map. +func (m *Ordered[K, T]) Set(key K, value T) { + if m == nil { + return + } + // Check if key already exists. + if _, found := m.values[key]; !found { + m.keys = append(m.keys, key) + } + m.values[key] = value +} + +// Get gets the value for the given key. +func (m *Ordered[K, T]) Get(key K) (T, bool) { + if m == nil { + var v T + return v, false + } + value, found := m.values[key] + return value, found +} + +// Has returns whether the given key exists in the map. +func (m *Ordered[K, T]) Has(key K) bool { + if m == nil { + return false + } + _, found := m.values[key] + return found +} + +// Delete deletes the value for the given key. +func (m *Ordered[K, T]) Delete(key K) { + if m == nil { + return + } + delete(m.values, key) + for i, k := range m.keys { + if k == key { + m.keys = slices.Delete(m.keys, i, i+1) + break + } + } +} + +// Clone creates a shallow copy of the map. +func (m *Ordered[K, T]) Clone() *Ordered[K, T] { + if m == nil { + return nil + } + clone := NewOrdered[K, T]() + for _, k := range m.keys { + clone.Set(k, m.values[k]) + } + return clone +} + +// Keys returns the keys in the order they were added. +func (m *Ordered[K, T]) Keys() []K { + if m == nil { + return nil + } + return m.keys +} + +// Values returns the values in the order they were added. +func (m *Ordered[K, T]) Values() []T { + if m == nil { + return nil + } + var values []T + for _, k := range m.keys { + values = append(values, m.values[k]) + } + return values +} + +// Len returns the number of items in the map. +func (m *Ordered[K, T]) Len() int { + if m == nil { + return 0 + } + return len(m.keys) +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// TODO(bep) replace with iter.Seq2 when we bump go Go 1.24. +func (m *Ordered[K, T]) Range(f func(key K, value T) bool) { + if m == nil { + return + } + for _, k := range m.keys { + if !f(k, m.values[k]) { + return + } + } +} + +// Hash calculates a hash from the values. +func (m *Ordered[K, T]) Hash() (uint64, error) { + if m == nil { + return 0, nil + } + return hashing.Hash(m.values) +} diff --git a/common/maps/ordered_test.go b/common/maps/ordered_test.go new file mode 100644 index 000000000..65a827810 --- /dev/null +++ b/common/maps/ordered_test.go @@ -0,0 +1,99 @@ +// Copyright 2024 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 maps + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestOrdered(t *testing.T) { + c := qt.New(t) + + m := NewOrdered[string, int]() + m.Set("a", 1) + m.Set("b", 2) + m.Set("c", 3) + + c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(m.Values(), qt.DeepEquals, []int{1, 2, 3}) + + v, found := m.Get("b") + c.Assert(found, qt.Equals, true) + c.Assert(v, qt.Equals, 2) + + m.Set("b", 22) + c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(m.Values(), qt.DeepEquals, []int{1, 22, 3}) + + m.Delete("b") + + c.Assert(m.Keys(), qt.DeepEquals, []string{"a", "c"}) + c.Assert(m.Values(), qt.DeepEquals, []int{1, 3}) +} + +func TestOrderedHash(t *testing.T) { + c := qt.New(t) + + m := NewOrdered[string, int]() + m.Set("a", 1) + m.Set("b", 2) + m.Set("c", 3) + + h1, err := m.Hash() + c.Assert(err, qt.IsNil) + + m.Set("d", 4) + + h2, err := m.Hash() + c.Assert(err, qt.IsNil) + + c.Assert(h1, qt.Not(qt.Equals), h2) + + m = NewOrdered[string, int]() + m.Set("b", 2) + m.Set("a", 1) + m.Set("c", 3) + + h3, err := m.Hash() + c.Assert(err, qt.IsNil) + // Order does not matter. + c.Assert(h1, qt.Equals, h3) +} + +func TestOrderedNil(t *testing.T) { + c := qt.New(t) + + var m *Ordered[string, int] + + m.Set("a", 1) + c.Assert(m.Keys(), qt.IsNil) + c.Assert(m.Values(), qt.IsNil) + v, found := m.Get("a") + c.Assert(found, qt.Equals, false) + c.Assert(v, qt.Equals, 0) + m.Delete("a") + var b bool + m.Range(func(k string, v int) bool { + b = true + return true + }) + c.Assert(b, qt.Equals, false) + c.Assert(m.Len(), qt.Equals, 0) + c.Assert(m.Clone(), qt.IsNil) + h, err := m.Hash() + c.Assert(err, qt.IsNil) + c.Assert(h, qt.Equals, uint64(0)) +} diff --git a/common/maps/params.go b/common/maps/params.go index d94d16f9d..819f796e4 100644 --- a/common/maps/params.go +++ b/common/maps/params.go @@ -61,7 +61,7 @@ func SetParams(dst, src Params) { // IsZero returns true if p is considered empty. func (p Params) IsZero() bool { - if p == nil || len(p) == 0 { + if len(p) == 0 { return true } @@ -74,7 +74,6 @@ func (p Params) IsZero() bool { } return false - } // MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given. @@ -93,7 +92,7 @@ func MergeParams(dst, src Params) { func (p Params) merge(ps ParamsMergeStrategy, pp Params) { ns, found := p.GetMergeStrategy() - var ms = ns + ms := ns if !found && ps != "" { ms = ps } @@ -248,7 +247,7 @@ const ( // CleanConfigStringMapString removes any processing instructions from m, // m will never be modified. func CleanConfigStringMapString(m map[string]string) map[string]string { - if m == nil || len(m) == 0 { + if len(m) == 0 { return m } if _, found := m[MergeStrategyKey]; !found { @@ -267,7 +266,7 @@ func CleanConfigStringMapString(m map[string]string) map[string]string { // CleanConfigStringMap is the same as CleanConfigStringMapString but for // map[string]any. func CleanConfigStringMap(m map[string]any) map[string]any { - if m == nil || len(m) == 0 { + if len(m) == 0 { return m } if _, found := m[MergeStrategyKey]; !found { @@ -291,7 +290,6 @@ func CleanConfigStringMap(m map[string]any) map[string]any { } return m2 - } func toMergeStrategy(v any) ParamsMergeStrategy { @@ -305,7 +303,7 @@ func toMergeStrategy(v any) ParamsMergeStrategy { } // PrepareParams -// * makes all the keys in the given map lower cased and will do so +// * makes all the keys in the given map lower cased and will do so recursively. // * This will modify the map given. // * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params. // * Any _merge value will be converted to proper type and value. @@ -345,3 +343,42 @@ func PrepareParams(m Params) { } } } + +// PrepareParamsClone is like PrepareParams, but it does not modify the input. +func PrepareParamsClone(m Params) Params { + m2 := make(Params) + for k, v := range m { + var retyped bool + lKey := strings.ToLower(k) + if lKey == MergeStrategyKey { + v = toMergeStrategy(v) + retyped = true + } else { + switch vv := v.(type) { + case map[any]any: + var p Params = cast.ToStringMap(v) + v = PrepareParamsClone(p) + retyped = true + case map[string]any: + var p Params = v.(map[string]any) + v = PrepareParamsClone(p) + retyped = true + case map[string]string: + p := make(Params) + for k, v := range vv { + p[k] = v + } + v = p + PrepareParams(p) + retyped = true + } + } + + if retyped || k != lKey { + m2[lKey] = v + } else { + m2[k] = v + } + } + return m2 +} diff --git a/common/maps/params_test.go b/common/maps/params_test.go index 578b2a576..892c77175 100644 --- a/common/maps/params_test.go +++ b/common/maps/params_test.go @@ -154,7 +154,6 @@ func TestParamsSetAndMerge(t *testing.T) { "a": "av", "c": "cv", }) - } func TestParamsIsZero(t *testing.T) { diff --git a/common/maps/scratch.go b/common/maps/scratch.go index e9f412540..cf5231783 100644 --- a/common/maps/scratch.go +++ b/common/maps/scratch.go @@ -22,31 +22,18 @@ import ( "github.com/gohugoio/hugo/common/math" ) -// Scratch is a writable context used for stateful operations in Page/Node rendering. +type StoreProvider interface { + // Store returns a Scratch that can be used to store temporary state. + // Store is not reset on server rebuilds. + Store() *Scratch +} + +// Scratch is a writable context used for stateful build operations type Scratch struct { values map[string]any mu sync.RWMutex } -// Scratcher provides a scratching service. -type Scratcher interface { - // Scratch returns a "scratch pad" that can be used to store state. - Scratch() *Scratch -} - -type scratcher struct { - s *Scratch -} - -func (s scratcher) Scratch() *Scratch { - return s.s -} - -// NewScratcher creates a new Scratcher. -func NewScratcher() Scratcher { - return scratcher{s: NewScratch()} -} - // Add will, for single values, add (using the + operator) the addend to the existing addend (if found). // Supports numeric values and strings. // diff --git a/common/maps/scratch_test.go b/common/maps/scratch_test.go index b515adb1d..f07169e61 100644 --- a/common/maps/scratch_test.go +++ b/common/maps/scratch_test.go @@ -140,7 +140,7 @@ func TestScratchInParallel(t *testing.T) { for i := 1; i <= 10; i++ { wg.Add(1) go func(j int) { - for k := 0; k < 10; k++ { + for k := range 10 { newVal := int64(k + j) _, err := scratch.Add(key, newVal) @@ -185,7 +185,7 @@ func TestScratchSetInMap(t *testing.T) { scratch.SetInMap("key", "zyx", "Zyx") scratch.SetInMap("key", "abc", "Abc (updated)") scratch.SetInMap("key", "def", "Def") - c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Abc (updated)", 1: "Def", 2: "Lux", 3: "Zyx"}) + c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Abc (updated)", "Def", "Lux", "Zyx"})) } func TestScratchDeleteInMap(t *testing.T) { @@ -199,7 +199,7 @@ func TestScratchDeleteInMap(t *testing.T) { scratch.DeleteInMap("key", "abc") scratch.SetInMap("key", "def", "Def") scratch.DeleteInMap("key", "lmn") // Do nothing - c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, []any{0: "Def", 1: "Lux", 2: "Zyx"}) + c.Assert(scratch.GetSortedMapValues("key"), qt.DeepEquals, any([]any{"Def", "Lux", "Zyx"})) } func TestScratchGetSortedMapValues(t *testing.T) { diff --git a/common/math/math.go b/common/math/math.go index d4e2c1148..f88fbcd9c 100644 --- a/common/math/math.go +++ b/common/math/math.go @@ -26,29 +26,32 @@ func DoArithmetic(a, b any, op rune) (any, error) { var ai, bi int64 var af, bf float64 var au, bu uint64 + var isInt, isFloat, isUint bool switch av.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: ai = av.Int() switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + isInt = true bi = bv.Int() case reflect.Float32, reflect.Float64: + isFloat = true af = float64(ai) // may overflow - ai = 0 bf = bv.Float() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: bu = bv.Uint() if ai >= 0 { + isUint = true au = uint64(ai) - ai = 0 } else { + isInt = true bi = int64(bu) // may overflow - bu = 0 } default: return nil, errors.New("can't apply the operator to the values") } case reflect.Float32, reflect.Float64: + isFloat = true af = av.Float() switch bv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -66,17 +69,18 @@ func DoArithmetic(a, b any, op rune) (any, error) { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: bi = bv.Int() if bi >= 0 { + isUint = true bu = uint64(bi) - bi = 0 } else { + isInt = true ai = int64(au) // may overflow - au = 0 } case reflect.Float32, reflect.Float64: + isFloat = true af = float64(au) // may overflow - au = 0 bf = bv.Float() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + isUint = true bu = bv.Uint() default: return nil, errors.New("can't apply the operator to the values") @@ -94,38 +98,32 @@ func DoArithmetic(a, b any, op rune) (any, error) { switch op { case '+': - if ai != 0 || bi != 0 { + if isInt { return ai + bi, nil - } else if af != 0 || bf != 0 { + } else if isFloat { return af + bf, nil - } else if au != 0 || bu != 0 { - return au + bu, nil } - return 0, nil + return au + bu, nil case '-': - if ai != 0 || bi != 0 { + if isInt { return ai - bi, nil - } else if af != 0 || bf != 0 { + } else if isFloat { return af - bf, nil - } else if au != 0 || bu != 0 { - return au - bu, nil } - return 0, nil + return au - bu, nil case '*': - if ai != 0 || bi != 0 { + if isInt { return ai * bi, nil - } else if af != 0 || bf != 0 { + } else if isFloat { return af * bf, nil - } else if au != 0 || bu != 0 { - return au * bu, nil } - return 0, nil + return au * bu, nil case '/': - if bi != 0 { + if isInt && bi != 0 { return ai / bi, nil - } else if bf != 0 { + } else if isFloat && bf != 0 { return af / bf, nil - } else if bu != 0 { + } else if isUint && bu != 0 { return au / bu, nil } return nil, errors.New("can't divide the value by 0") diff --git a/common/math/math_test.go b/common/math/math_test.go index 89e391ce0..d75d30a69 100644 --- a/common/math/math_test.go +++ b/common/math/math_test.go @@ -30,10 +30,12 @@ func TestDoArithmetic(t *testing.T) { expect any }{ {3, 2, '+', int64(5)}, + {0, 0, '+', int64(0)}, {3, 2, '-', int64(1)}, {3, 2, '*', int64(6)}, {3, 2, '/', int64(1)}, {3.0, 2, '+', float64(5)}, + {0.0, 0, '+', float64(0.0)}, {3.0, 2, '-', float64(1)}, {3.0, 2, '*', float64(6)}, {3.0, 2, '/', float64(1.5)}, @@ -42,18 +44,22 @@ func TestDoArithmetic(t *testing.T) { {3, 2.0, '*', float64(6)}, {3, 2.0, '/', float64(1.5)}, {3.0, 2.0, '+', float64(5)}, + {0.0, 0.0, '+', float64(0.0)}, {3.0, 2.0, '-', float64(1)}, {3.0, 2.0, '*', float64(6)}, {3.0, 2.0, '/', float64(1.5)}, {uint(3), uint(2), '+', uint64(5)}, + {uint(0), uint(0), '+', uint64(0)}, {uint(3), uint(2), '-', uint64(1)}, {uint(3), uint(2), '*', uint64(6)}, {uint(3), uint(2), '/', uint64(1)}, {uint(3), 2, '+', uint64(5)}, + {uint(0), 0, '+', uint64(0)}, {uint(3), 2, '-', uint64(1)}, {uint(3), 2, '*', uint64(6)}, {uint(3), 2, '/', uint64(1)}, {3, uint(2), '+', uint64(5)}, + {0, uint(0), '+', uint64(0)}, {3, uint(2), '-', uint64(1)}, {3, uint(2), '*', uint64(6)}, {3, uint(2), '/', uint64(1)}, @@ -66,16 +72,15 @@ func TestDoArithmetic(t *testing.T) { {-3, uint(2), '*', int64(-6)}, {-3, uint(2), '/', int64(-1)}, {uint(3), 2.0, '+', float64(5)}, + {uint(0), 0.0, '+', float64(0)}, {uint(3), 2.0, '-', float64(1)}, {uint(3), 2.0, '*', float64(6)}, {uint(3), 2.0, '/', float64(1.5)}, {3.0, uint(2), '+', float64(5)}, + {0.0, uint(0), '+', float64(0)}, {3.0, uint(2), '-', float64(1)}, {3.0, uint(2), '*', float64(6)}, {3.0, uint(2), '/', float64(1.5)}, - {0, 0, '+', 0}, - {0, 0, '-', 0}, - {0, 0, '*', 0}, {"foo", "bar", '+', "foobar"}, {3, 0, '/', false}, {3.0, 0, '/', false}, diff --git a/common/para/para_test.go b/common/para/para_test.go index 646b7b36b..cf24a4e37 100644 --- a/common/para/para_test.go +++ b/common/para/para_test.go @@ -32,8 +32,9 @@ func TestPara(t *testing.T) { t.Skipf("skip para test, CPU count is %d", runtime.NumCPU()) } - if !htesting.IsCI() { - t.Skip("skip para test when not running on CI") + // TODO(bep) + if htesting.IsCI() { + t.Skip("skip para test when running on CI") } c := qt.New(t) @@ -41,7 +42,7 @@ func TestPara(t *testing.T) { c.Run("Order", func(c *qt.C) { n := 500 ints := make([]int, n) - for i := 0; i < n; i++ { + for i := range n { ints[i] = i } @@ -50,7 +51,7 @@ func TestPara(t *testing.T) { var result []int var mu sync.Mutex - for i := 0; i < n; i++ { + for i := range n { i := i r.Run(func() error { mu.Lock() @@ -77,7 +78,7 @@ func TestPara(t *testing.T) { var counter int64 - for i := 0; i < n; i++ { + for range n { r.Run(func() error { atomic.AddInt64(&counter, 1) time.Sleep(1 * time.Millisecond) diff --git a/common/paths/path.go b/common/paths/path.go index f1992f196..de91d6a2f 100644 --- a/common/paths/path.go +++ b/common/paths/path.go @@ -16,14 +16,18 @@ package paths import ( "errors" "fmt" + "net/url" "path" "path/filepath" - "regexp" "strings" + "unicode" ) // FilePathSeparator as defined by os.Separator. -const FilePathSeparator = string(filepath.Separator) +const ( + FilePathSeparator = string(filepath.Separator) + slash = "/" +) // filepathPathBridge is a bridge for common functionality in filepath vs path type filepathPathBridge interface { @@ -72,6 +76,30 @@ func AbsPathify(workingDir, inPath string) string { return filepath.Join(workingDir, inPath) } +// AddTrailingSlash adds a trailing Unix styled slash (/) if not already +// there. +func AddTrailingSlash(path string) string { + if !strings.HasSuffix(path, "/") { + path += "/" + } + return path +} + +// AddLeadingSlash adds a leading Unix styled slash (/) if not already +// there. +func AddLeadingSlash(path string) string { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + return path +} + +// AddTrailingAndLeadingSlash adds a leading and trailing Unix styled slash (/) if not already +// there. +func AddLeadingAndTrailingSlash(path string) string { + return AddTrailingSlash(AddLeadingSlash(path)) +} + // MakeTitle converts the path given to a suitable title, trimming whitespace // and replacing hyphens with whitespace. func MakeTitle(inpath string) string { @@ -94,43 +122,6 @@ func makePathRelative(inPath string, possibleDirectories ...string) (string, err return inPath, errors.New("can't extract relative path, unknown prefix") } -// Should be good enough for Hugo. -var isFileRe = regexp.MustCompile(`.*\..{1,6}$`) - -// GetDottedRelativePath expects a relative path starting after the content directory. -// It returns a relative path with dots ("..") navigating up the path structure. -func GetDottedRelativePath(inPath string) string { - inPath = path.Clean(filepath.ToSlash(inPath)) - - if inPath == "." { - return "./" - } - - if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, "/") { - inPath += "/" - } - - if !strings.HasPrefix(inPath, "/") { - inPath = "/" + inPath - } - - dir, _ := filepath.Split(inPath) - - sectionCount := strings.Count(dir, "/") - - if sectionCount == 0 || dir == "/" { - return "./" - } - - var dottedPath string - - for i := 1; i < sectionCount; i++ { - dottedPath += "../" - } - - return dottedPath -} - // ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md". func ExtNoDelimiter(in string) string { return strings.TrimPrefix(Ext(in), ".") @@ -167,12 +158,6 @@ func Filename(in string) (name string) { return } -// PathNoExt takes a path, strips out the extension, -// and returns the name of the file. -func PathNoExt(in string) string { - return strings.TrimSuffix(in, path.Ext(in)) -} - // FileAndExt returns the filename and any extension of a file path as // two separate strings. // @@ -209,7 +194,7 @@ func extractFilename(in, ext, base, pathSeparator string) (name string) { // return the filename minus the extension (and the ".") name = base[:strings.LastIndex(base, ".")] } else { - // no extension case so just return base, which willi + // no extension case so just return base, which will // be the filename name = base } @@ -252,16 +237,136 @@ func prettifyPath(in string, b filepathPathBridge) string { return b.Join(b.Dir(in), name, "index"+ext) } -type NamedSlice struct { - Name string - Slice []string +// CommonDirPath returns the common directory of the given paths. +func CommonDirPath(path1, path2 string) string { + if path1 == "" || path2 == "" { + return "" + } + + hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/") + + path1 = TrimLeading(path1) + path2 = TrimLeading(path2) + + p1 := strings.Split(path1, "/") + p2 := strings.Split(path2, "/") + + var common []string + + for i := 0; i < len(p1) && i < len(p2); i++ { + if p1[i] == p2[i] { + common = append(common, p1[i]) + } else { + break + } + } + + s := strings.Join(common, "/") + + if hadLeadingSlash && s != "" { + s = "/" + s + } + + return s } -func (n NamedSlice) String() string { - if len(n.Slice) == 0 { - return n.Name +// Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only +// a predefined set of special Unicode characters. +// +// Spaces will be replaced with a single hyphen. +// +// This function is the core function used to normalize paths in Hugo. +// +// Note that this is the first common step for URL/path sanitation, +// the final URL/path may end up looking differently if the user has stricter rules defined (e.g. removePathAccents=true). +func Sanitize(s string) string { + var willChange bool + for i, r := range s { + willChange = !isAllowedPathCharacter(s, i, r) + if willChange { + break + } } - return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ",")) + + if !willChange { + // Prevent allocation when nothing changes. + return s + } + + target := make([]rune, 0, len(s)) + var ( + prependHyphen bool + wasHyphen bool + ) + + for i, r := range s { + isAllowed := isAllowedPathCharacter(s, i, r) + + if isAllowed { + // track explicit hyphen in input; no need to add a new hyphen if + // we just saw one. + wasHyphen = r == '-' + + if prependHyphen { + // if currently have a hyphen, don't prepend an extra one + if !wasHyphen { + target = append(target, '-') + } + prependHyphen = false + } + target = append(target, r) + } else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) { + prependHyphen = true + } + } + + return string(target) +} + +func isAllowedPathCharacter(s string, i int, r rune) bool { + if r == ' ' { + return false + } + // Check for the most likely first (faster). + isAllowed := unicode.IsLetter(r) || unicode.IsDigit(r) + isAllowed = isAllowed || r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-' || r == '@' + isAllowed = isAllowed || unicode.IsMark(r) + isAllowed = isAllowed || (r == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2])) + return isAllowed +} + +// From https://golang.org/src/net/url/url.go +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +var slashFunc = func(r rune) bool { + return r == '/' +} + +// Dir behaves like path.Dir without the path.Clean step. +// +// The returned path ends in a slash only if it is the root "/". +func Dir(s string) string { + dir, _ := path.Split(s) + if len(dir) > 1 && dir[len(dir)-1] == '/' { + return dir[:len(dir)-1] + } + return dir +} + +// FieldsSlash cuts s into fields separated with '/'. +func FieldsSlash(s string) []string { + f := strings.FieldsFunc(s, slashFunc) + return f } // DirFile holds the result from path.Split. @@ -274,3 +379,52 @@ type DirFile struct { func (df DirFile) String() string { return fmt.Sprintf("%s|%s", df.Dir, df.File) } + +// PathEscape escapes unicode letters in pth. +// Use URLEscape to escape full URLs including scheme, query etc. +// This is slightly faster for the common case. +// Note, there is a url.PathEscape function, but that also +// escapes /. +func PathEscape(pth string) string { + u, err := url.Parse(pth) + if err != nil { + panic(err) + } + return u.EscapedPath() +} + +// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer. +func ToSlashTrimLeading(s string) string { + return TrimLeading(filepath.ToSlash(s)) +} + +// TrimLeading trims the leading slash from the given string. +func TrimLeading(s string) string { + return strings.TrimPrefix(s, "/") +} + +// ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer. +func ToSlashTrimTrailing(s string) string { + return TrimTrailing(filepath.ToSlash(s)) +} + +// TrimTrailing trims the trailing slash from the given string. +func TrimTrailing(s string) string { + return strings.TrimSuffix(s, "/") +} + +// ToSlashTrim trims any leading and trailing slashes from the given string and converts it to a forward slash separated path. +func ToSlashTrim(s string) string { + return strings.Trim(filepath.ToSlash(s), "/") +} + +// ToSlashPreserveLeading converts the path given to a forward slash separated path +// and preserves the leading slash if present trimming any trailing slash. +func ToSlashPreserveLeading(s string) string { + return "/" + strings.Trim(filepath.ToSlash(s), "/") +} + +// IsSameFilePath checks if s1 and s2 are the same file path. +func IsSameFilePath(s1, s2 string) bool { + return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2)) +} diff --git a/common/paths/path_test.go b/common/paths/path_test.go index 2400f16ab..bc27df6c6 100644 --- a/common/paths/path_test.go +++ b/common/paths/path_test.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -75,44 +75,6 @@ func TestMakePathRelative(t *testing.T) { } } -func TestGetDottedRelativePath(t *testing.T) { - // on Windows this will receive both kinds, both country and western ... - for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} { - doTestGetDottedRelativePath(f, t) - } -} - -func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) { - type test struct { - input, expected string - } - data := []test{ - {"", "./"}, - {urlFixer("/"), "./"}, - {urlFixer("post"), "../"}, - {urlFixer("/post"), "../"}, - {urlFixer("post/"), "../"}, - {urlFixer("tags/foo.html"), "../"}, - {urlFixer("/tags/foo.html"), "../"}, - {urlFixer("/post/"), "../"}, - {urlFixer("////post/////"), "../"}, - {urlFixer("/foo/bar/index.html"), "../../"}, - {urlFixer("/foo/bar/foo/"), "../../../"}, - {urlFixer("/foo/bar/foo"), "../../../"}, - {urlFixer("foo/bar/foo/"), "../../../"}, - {urlFixer("foo/bar/foo/bar"), "../../../../"}, - {"404.html", "./"}, - {"404.xml", "./"}, - {"/404.html", "./"}, - } - for i, d := range data { - output := GetDottedRelativePath(d.input) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - func TestMakeTitle(t *testing.T) { type test struct { input, expected string @@ -226,3 +188,126 @@ func TestFileAndExt(t *testing.T) { } } } + +func TestSanitize(t *testing.T) { + c := qt.New(t) + tests := []struct { + input string + expected string + }{ + {" Foo bar ", "Foo-bar"}, + {"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo"}, + {"fOO,bar:foobAR", "fOObarfoobAR"}, + {"FOo/BaR.html", "FOo/BaR.html"}, + {"FOo/Ba---R.html", "FOo/Ba---R.html"}, /// See #10104 + {"FOo/Ba R.html", "FOo/Ba-R.html"}, + {"трям/трям", "трям/трям"}, + {"은행", "은행"}, + {"Банковский кассир", "Банковский-кассир"}, + // Issue #1488 + {"संस्कृत", "संस्कृत"}, + {"a%C3%B1ame", "a%C3%B1ame"}, // Issue #1292 + {"this+is+a+test", "this+is+a+test"}, // Issue #1290 + {"~foo", "~foo"}, // Issue #2177 + + } + + for _, test := range tests { + c.Assert(Sanitize(test.input), qt.Equals, test.expected) + } +} + +func BenchmarkSanitize(b *testing.B) { + const ( + allAlowedPath = "foo/bar" + spacePath = "foo bar" + ) + + // This should not allocate any memory. + b.Run("All allowed", func(b *testing.B) { + for i := 0; i < b.N; i++ { + got := Sanitize(allAlowedPath) + if got != allAlowedPath { + b.Fatal(got) + } + } + }) + + // This will allocate some memory. + b.Run("Spaces", func(b *testing.B) { + for i := 0; i < b.N; i++ { + got := Sanitize(spacePath) + if got != "foo-bar" { + b.Fatal(got) + } + } + }) +} + +func TestDir(t *testing.T) { + c := qt.New(t) + c.Assert(Dir("/a/b/c/d"), qt.Equals, "/a/b/c") + c.Assert(Dir("/a"), qt.Equals, "/") + c.Assert(Dir("/"), qt.Equals, "/") + c.Assert(Dir(""), qt.Equals, "") +} + +func TestFieldsSlash(t *testing.T) { + c := qt.New(t) + + c.Assert(FieldsSlash("a/b/c"), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(FieldsSlash("/a/b/c"), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(FieldsSlash("/a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(FieldsSlash("a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"}) + c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{}) + c.Assert(FieldsSlash(""), qt.DeepEquals, []string{}) +} + +func TestCommonDirPath(t *testing.T) { + c := qt.New(t) + + for _, this := range []struct { + a, b, expected string + }{ + {"/a/b/c", "/a/b/d", "/a/b"}, + {"/a/b/c", "a/b/d", "/a/b"}, + {"a/b/c", "/a/b/d", "/a/b"}, + {"a/b/c", "a/b/d", "a/b"}, + {"/a/b/c", "/a/b/c", "/a/b/c"}, + {"/a/b/c", "/a/b/c/d", "/a/b/c"}, + {"/a/b/c", "/a/b", "/a/b"}, + {"/a/b/c", "/a", "/a"}, + {"/a/b/c", "/d/e/f", ""}, + } { + c.Assert(CommonDirPath(this.a, this.b), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b)) + } +} + +func TestIsSameFilePath(t *testing.T) { + c := qt.New(t) + + for _, this := range []struct { + a, b string + expected bool + }{ + {"/a/b/c", "/a/b/c", true}, + {"/a/b/c", "/a/b/c/", true}, + {"/a/b/c", "/a/b/d", false}, + {"/a/b/c", "/a/b", false}, + {"/a/b/c", "/a/b/c/d", false}, + {"/a/b/c", "/a/b/cd", false}, + {"/a/b/c", "/a/b/cc", false}, + {"/a/b/c", "/a/b/c/", true}, + {"/a/b/c", "/a/b/c//", true}, + {"/a/b/c", "/a/b/c/.", true}, + {"/a/b/c", "/a/b/c/./", true}, + {"/a/b/c", "/a/b/c/./.", true}, + {"/a/b/c", "/a/b/c/././", true}, + {"/a/b/c", "/a/b/c/././.", true}, + {"/a/b/c", "/a/b/c/./././", true}, + {"/a/b/c", "/a/b/c/./././.", true}, + {"/a/b/c", "/a/b/c/././././", true}, + } { + c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b)) + } +} diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go new file mode 100644 index 000000000..8b9259bf7 --- /dev/null +++ b/common/paths/pathparser.go @@ -0,0 +1,788 @@ +// Copyright 2024 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 paths + +import ( + "path" + "path/filepath" + "runtime" + "strings" + "sync" + + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/resources/kinds" +) + +const ( + identifierBaseof = "baseof" +) + +// PathParser parses a path into a Path. +type PathParser struct { + // Maps the language code to its index in the languages/sites slice. + LanguageIndex map[string]int + + // Reports whether the given language is disabled. + IsLangDisabled func(string) bool + + // IsOutputFormat reports whether the given name is a valid output format. + // The second argument is optional. + IsOutputFormat func(name, ext string) bool + + // Reports whether the given ext is a content file. + IsContentExt func(string) bool +} + +// NormalizePathString returns a normalized path string using the very basic Hugo rules. +func NormalizePathStringBasic(s string) string { + // All lower case. + s = strings.ToLower(s) + + // Replace spaces with hyphens. + s = strings.ReplaceAll(s, " ", "-") + + return s +} + +// ParseIdentity parses component c with path s into a StringIdentity. +func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity { + p := pp.parsePooled(c, s) + defer putPath(p) + return identity.StringIdentity(p.IdentifierBase()) +} + +// ParseBaseAndBaseNameNoIdentifier parses component c with path s into a base and a base name without any identifier. +func (pp *PathParser) ParseBaseAndBaseNameNoIdentifier(c, s string) (string, string) { + p := pp.parsePooled(c, s) + defer putPath(p) + return p.Base(), p.BaseNameNoIdentifier() +} + +func (pp *PathParser) parsePooled(c, s string) *Path { + s = NormalizePathStringBasic(s) + p := getPath() + p.component = c + p, err := pp.doParse(c, s, p) + if err != nil { + panic(err) + } + return p +} + +// Parse parses component c with path s into Path using Hugo's content path rules. +func (pp *PathParser) Parse(c, s string) *Path { + p, err := pp.parse(c, s) + if err != nil { + panic(err) + } + return p +} + +func (pp *PathParser) newPath(component string) *Path { + p := &Path{} + p.reset() + p.component = component + return p +} + +func (pp *PathParser) parse(component, s string) (*Path, error) { + ss := NormalizePathStringBasic(s) + + p, err := pp.doParse(component, ss, pp.newPath(component)) + if err != nil { + return nil, err + } + + if s != ss { + var err error + // Preserve the original case for titles etc. + p.unnormalized, err = pp.doParse(component, s, pp.newPath(component)) + if err != nil { + return nil, err + } + } else { + p.unnormalized = p + } + + return p, nil +} + +func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) { + if p.posContainerHigh != -1 { + return + } + mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil + mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) + mayHaveOutputFormat := component == files.ComponentFolderLayouts + mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat + var mayHaveLayout bool + if p.pathType == TypeShortcode { + mayHaveLayout = !isLast && component == files.ComponentFolderLayouts + } else { + mayHaveLayout = component == files.ComponentFolderLayouts + } + + var found bool + var high int + if len(p.identifiersKnown) > 0 { + high = lastDot + } else { + high = len(p.s) + } + id := types.LowHigh[string]{Low: i + 1, High: high} + sid := p.s[id.Low:id.High] + + if len(p.identifiersKnown) == 0 { + // The first is always the extension. + p.identifiersKnown = append(p.identifiersKnown, id) + found = true + + // May also be the output format. + if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") { + p.posIdentifierOutputFormat = 0 + } + } else { + + var langFound bool + + if mayHaveLang { + var disabled bool + _, langFound = pp.LanguageIndex[sid] + if !langFound { + disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid) + if disabled { + p.disabled = true + langFound = true + } + } + found = langFound + if langFound { + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierLanguage = len(p.identifiersKnown) - 1 + } + } + + if !found && mayHaveOutputFormat { + // At this point we may already have resolved an output format, + // but we need to keep looking for a more specific one, e.g. amp before html. + // Use both name and extension to prevent + // false positives on the form css.html. + if pp.IsOutputFormat(sid, p.Ext()) { + found = true + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1 + } + } + + if !found && mayHaveKind { + if kinds.GetKindMain(sid) != "" { + found = true + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierKind = len(p.identifiersKnown) - 1 + } + } + + if !found && sid == identifierBaseof { + found = true + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierBaseof = len(p.identifiersKnown) - 1 + } + + if !found && mayHaveLayout { + p.identifiersKnown = append(p.identifiersKnown, id) + p.posIdentifierLayout = len(p.identifiersKnown) - 1 + found = true + } + + if !found { + p.identifiersUnknown = append(p.identifiersUnknown, id) + } + + } +} + +func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { + if runtime.GOOS == "windows" { + s = path.Clean(filepath.ToSlash(s)) + if s == "." { + s = "" + } + } + + if s == "" { + s = "/" + } + + // Leading slash, no trailing slash. + if !strings.HasPrefix(s, "/") { + s = "/" + s + } + + if s != "/" && s[len(s)-1] == '/' { + s = s[:len(s)-1] + } + + p.s = s + slashCount := 0 + lastDot := 0 + lastSlashIdx := strings.LastIndex(s, "/") + numDots := strings.Count(s[lastSlashIdx+1:], ".") + if strings.Contains(s, "/_shortcodes/") { + p.pathType = TypeShortcode + } + + for i := len(s) - 1; i >= 0; i-- { + c := s[i] + + switch c { + case '.': + pp.parseIdentifier(component, s, p, i, lastDot, numDots, false) + lastDot = i + case '/': + slashCount++ + if p.posContainerHigh == -1 { + if lastDot > 0 { + pp.parseIdentifier(component, s, p, i, lastDot, numDots, true) + } + p.posContainerHigh = i + 1 + } else if p.posContainerLow == -1 { + p.posContainerLow = i + 1 + } + if i > 0 { + p.posSectionHigh = i + } + } + } + + if len(p.identifiersKnown) > 0 { + isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes + isContent := isContentComponent && pp.IsContentExt(p.Ext()) + id := p.identifiersKnown[len(p.identifiersKnown)-1] + + if id.Low > p.posContainerHigh { + b := p.s[p.posContainerHigh : id.Low-1] + if isContent { + switch b { + case "index": + p.pathType = TypeLeaf + case "_index": + p.pathType = TypeBranch + default: + p.pathType = TypeContentSingle + } + + if slashCount == 2 && p.IsLeafBundle() { + p.posSectionHigh = 0 + } + } else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) { + p.pathType = TypeContentData + } + } + } + + if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts { + if p.posIdentifierBaseof != -1 { + p.pathType = TypeBaseof + } else { + pth := p.Path() + if strings.Contains(pth, "/_shortcodes/") { + p.pathType = TypeShortcode + } else if strings.Contains(pth, "/_markup/") { + p.pathType = TypeMarkup + } else if strings.HasPrefix(pth, "/_partials/") { + p.pathType = TypePartial + } + } + } + + if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 { + id := p.identifiersKnown[p.posIdentifierLayout] + if id.Low == p.posContainerHigh { + // First identifier is shortcode name. + p.posIdentifierLayout = -1 + } + } + + return p, nil +} + +func ModifyPathBundleTypeResource(p *Path) { + if p.IsContent() { + p.pathType = TypeContentResource + } else { + p.pathType = TypeFile + } +} + +//go:generate stringer -type Type + +type Type int + +const ( + + // A generic resource, e.g. a JSON file. + TypeFile Type = iota + + // All below are content files. + // A resource of a content type with front matter. + TypeContentResource + + // E.g. /blog/my-post.md + TypeContentSingle + + // All below are bundled content files. + + // Leaf bundles, e.g. /blog/my-post/index.md + TypeLeaf + + // Branch bundles, e.g. /blog/_index.md + TypeBranch + + // Content data file, _content.gotmpl. + TypeContentData + + // Layout types. + TypeMarkup + TypeShortcode + TypePartial + TypeBaseof +) + +type Path struct { + // Note: Any additions to this struct should also be added to the pathPool. + s string + + posContainerLow int + posContainerHigh int + posSectionHigh int + + component string + pathType Type + + identifiersKnown []types.LowHigh[string] + identifiersUnknown []types.LowHigh[string] + + posIdentifierLanguage int + posIdentifierOutputFormat int + posIdentifierKind int + posIdentifierLayout int + posIdentifierBaseof int + disabled bool + + trimLeadingSlash bool + + unnormalized *Path +} + +var pathPool = &sync.Pool{ + New: func() any { + p := &Path{} + p.reset() + return p + }, +} + +func getPath() *Path { + return pathPool.Get().(*Path) +} + +func putPath(p *Path) { + p.reset() + pathPool.Put(p) +} + +func (p *Path) reset() { + p.s = "" + p.posContainerLow = -1 + p.posContainerHigh = -1 + p.posSectionHigh = -1 + p.component = "" + p.pathType = 0 + p.identifiersKnown = p.identifiersKnown[:0] + p.posIdentifierLanguage = -1 + p.posIdentifierOutputFormat = -1 + p.posIdentifierKind = -1 + p.posIdentifierLayout = -1 + p.posIdentifierBaseof = -1 + p.disabled = false + p.trimLeadingSlash = false + p.unnormalized = nil +} + +// TrimLeadingSlash returns a copy of the Path with the leading slash removed. +func (p Path) TrimLeadingSlash() *Path { + p.trimLeadingSlash = true + return &p +} + +func (p *Path) norm(s string) string { + if p.trimLeadingSlash { + s = strings.TrimPrefix(s, "/") + } + return s +} + +// IdentifierBase satisfies identity.Identity. +func (p *Path) IdentifierBase() string { + if p.Component() == files.ComponentFolderLayouts { + return p.Path() + } + return p.Base() +} + +// Component returns the component for this path (e.g. "content"). +func (p *Path) Component() string { + return p.component +} + +// Container returns the base name of the container directory for this path. +func (p *Path) Container() string { + if p.posContainerLow == -1 { + return "" + } + return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1]) +} + +func (p *Path) String() string { + if p == nil { + return "" + } + return p.Path() +} + +// ContainerDir returns the container directory for this path. +// For content bundles this will be the parent directory. +func (p *Path) ContainerDir() string { + if p.posContainerLow == -1 || !p.IsBundle() { + return p.Dir() + } + return p.norm(p.s[:p.posContainerLow-1]) +} + +// Section returns the first path element (section). +func (p *Path) Section() string { + if p.posSectionHigh <= 0 { + return "" + } + return p.norm(p.s[1:p.posSectionHigh]) +} + +// IsContent returns true if the path is a content file (e.g. mypost.md). +// Note that this will also return true for content files in a bundle. +func (p *Path) IsContent() bool { + return p.Type() >= TypeContentResource && p.Type() <= TypeContentData +} + +// isContentPage returns true if the path is a content file (e.g. mypost.md), +// but nof if inside a leaf bundle. +func (p *Path) isContentPage() bool { + return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData +} + +// Name returns the last element of path. +func (p *Path) Name() string { + if p.posContainerHigh > 0 { + return p.s[p.posContainerHigh:] + } + return p.s +} + +// Name returns the last element of path without any extension. +func (p *Path) NameNoExt() string { + if i := p.identifierIndex(0); i != -1 { + return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1] + } + return p.s[p.posContainerHigh:] +} + +// Name returns the last element of path without any language identifier. +func (p *Path) NameNoLang() string { + i := p.identifierIndex(p.posIdentifierLanguage) + if i == -1 { + return p.Name() + } + + return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:] +} + +// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension). +// For bundles this will be the containing directory's name, e.g. "blog". +func (p *Path) BaseNameNoIdentifier() string { + if p.IsBundle() { + return p.Container() + } + return p.NameNoIdentifier() +} + +// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension). +func (p *Path) NameNoIdentifier() string { + lowHigh := p.nameLowHigh() + return p.s[lowHigh.Low:lowHigh.High] +} + +func (p *Path) nameLowHigh() types.LowHigh[string] { + if len(p.identifiersKnown) > 0 { + lastID := p.identifiersKnown[len(p.identifiersKnown)-1] + if p.posContainerHigh == lastID.Low { + // The last identifier is the name. + return lastID + } + return types.LowHigh[string]{ + Low: p.posContainerHigh, + High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1, + } + } + return types.LowHigh[string]{ + Low: p.posContainerHigh, + High: len(p.s), + } +} + +// Dir returns all but the last element of path, typically the path's directory. +func (p *Path) Dir() (d string) { + if p.posContainerHigh > 0 { + d = p.s[:p.posContainerHigh-1] + } + if d == "" { + d = "/" + } + d = p.norm(d) + return +} + +// Path returns the full path. +func (p *Path) Path() (d string) { + return p.norm(p.s) +} + +// PathNoLeadingSlash returns the full path without the leading slash. +func (p *Path) PathNoLeadingSlash() string { + return p.Path()[1:] +} + +// Unnormalized returns the Path with the original case preserved. +func (p *Path) Unnormalized() *Path { + return p.unnormalized +} + +// PathNoLang returns the Path but with any language identifier removed. +func (p *Path) PathNoLang() string { + return p.base(true, false) +} + +// PathNoIdentifier returns the Path but with any identifier (ext, lang) removed. +func (p *Path) PathNoIdentifier() string { + return p.base(false, false) +} + +// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format. +func (p *Path) PathBeforeLangAndOutputFormatAndExt() string { + if len(p.identifiersKnown) == 0 { + return p.norm(p.s) + } + i := p.identifierIndex(0) + + if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) { + i = j + } + if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) { + i = j + } + + if i == -1 { + return p.norm(p.s) + } + + id := p.identifiersKnown[i] + return p.norm(p.s[:id.Low-1]) +} + +// PathRel returns the path relative to the given owner. +func (p *Path) PathRel(owner *Path) string { + ob := owner.Base() + if !strings.HasSuffix(ob, "/") { + ob += "/" + } + return strings.TrimPrefix(p.Path(), ob) +} + +// BaseRel returns the base path relative to the given owner. +func (p *Path) BaseRel(owner *Path) string { + ob := owner.Base() + if ob == "/" { + ob = "" + } + return p.Base()[len(ob)+1:] +} + +// For content files, Base returns the path without any identifiers (extension, language code etc.). +// Any 'index' as the last path element is ignored. +// +// For other files (Resources), any extension is kept. +func (p *Path) Base() string { + return p.base(!p.isContentPage(), p.IsBundle()) +} + +// Used in template lookups. +// For pages with Type set, we treat that as the section. +func (p *Path) BaseReTyped(typ string) (d string) { + base := p.Base() + if p.Section() == typ { + return base + } + d = "/" + typ + if p.posSectionHigh != -1 { + d += base[p.posSectionHigh:] + } + d = p.norm(d) + return +} + +// BaseNoLeadingSlash returns the base path without the leading slash. +func (p *Path) BaseNoLeadingSlash() string { + return p.Base()[1:] +} + +func (p *Path) base(preserveExt, isBundle bool) string { + if len(p.identifiersKnown) == 0 { + return p.norm(p.s) + } + + if preserveExt && len(p.identifiersKnown) == 1 { + // Preserve extension. + return p.norm(p.s) + } + + var high int + + if isBundle { + high = p.posContainerHigh - 1 + } else { + high = p.nameLowHigh().High + } + + if high == 0 { + high++ + } + + if !preserveExt { + return p.norm(p.s[:high]) + } + + // For txt files etc. we want to preserve the extension. + id := p.identifiersKnown[0] + + return p.norm(p.s[:high] + p.s[id.Low-1:id.High]) +} + +func (p *Path) Ext() string { + return p.identifierAsString(0) +} + +func (p *Path) OutputFormat() string { + return p.identifierAsString(p.posIdentifierOutputFormat) +} + +func (p *Path) Kind() string { + return p.identifierAsString(p.posIdentifierKind) +} + +func (p *Path) Layout() string { + return p.identifierAsString(p.posIdentifierLayout) +} + +func (p *Path) Lang() string { + return p.identifierAsString(p.posIdentifierLanguage) +} + +func (p *Path) Identifier(i int) string { + return p.identifierAsString(i) +} + +func (p *Path) Disabled() bool { + return p.disabled +} + +func (p *Path) Identifiers() []string { + ids := make([]string, len(p.identifiersKnown)) + for i, id := range p.identifiersKnown { + ids[i] = p.s[id.Low:id.High] + } + return ids +} + +func (p *Path) IdentifiersUnknown() []string { + ids := make([]string, len(p.identifiersUnknown)) + for i, id := range p.identifiersUnknown { + ids[i] = p.s[id.Low:id.High] + } + return ids +} + +func (p *Path) Type() Type { + return p.pathType +} + +func (p *Path) IsBundle() bool { + return p.pathType >= TypeLeaf && p.pathType <= TypeContentData +} + +func (p *Path) IsBranchBundle() bool { + return p.pathType == TypeBranch +} + +func (p *Path) IsLeafBundle() bool { + return p.pathType == TypeLeaf +} + +func (p *Path) IsContentData() bool { + return p.pathType == TypeContentData +} + +func (p Path) ForType(t Type) *Path { + p.pathType = t + return &p +} + +func (p *Path) identifierAsString(i int) string { + i = p.identifierIndex(i) + if i == -1 { + return "" + } + + id := p.identifiersKnown[i] + return p.s[id.Low:id.High] +} + +func (p *Path) identifierIndex(i int) int { + if i < 0 || i >= len(p.identifiersKnown) { + return -1 + } + return i +} + +// HasExt returns true if the Unix styled path has an extension. +func HasExt(p string) bool { + for i := len(p) - 1; i >= 0; i-- { + if p[i] == '.' { + return true + } + if p[i] == '/' { + return false + } + } + return false +} diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go new file mode 100644 index 000000000..b1734aef2 --- /dev/null +++ b/common/paths/pathparser_test.go @@ -0,0 +1,611 @@ +// Copyright 2024 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 paths + +import ( + "path/filepath" + "testing" + + "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/resources/kinds" + + qt "github.com/frankban/quicktest" +) + +var testParser = &PathParser{ + LanguageIndex: map[string]int{ + "no": 0, + "en": 1, + "fr": 2, + }, + IsContentExt: func(ext string) bool { + return ext == "md" + }, + IsOutputFormat: func(name, ext string) bool { + switch name { + case "html", "amp", "csv", "rss": + return true + } + return false + }, +} + +func TestParse(t *testing.T) { + c := qt.New(t) + + tests := []struct { + name string + path string + assert func(c *qt.C, p *Path) + }{ + { + "Basic text file", + "/a/b.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b.txt") + c.Assert(p.Base(), qt.Equals, "/a/b.txt") + c.Assert(p.Container(), qt.Equals, "a") + c.Assert(p.Dir(), qt.Equals, "/a") + c.Assert(p.Ext(), qt.Equals, "txt") + c.Assert(p.IsContent(), qt.IsFalse) + }, + }, + { + "Basic text file, upper case", + "/A/B.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b.txt") + c.Assert(p.NameNoExt(), qt.Equals, "b") + c.Assert(p.NameNoIdentifier(), qt.Equals, "b") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") + c.Assert(p.Base(), qt.Equals, "/a/b.txt") + c.Assert(p.Ext(), qt.Equals, "txt") + }, + }, + { + "Basic text file, 1 space in dir", + "/a b/c.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a-b/c.txt") + }, + }, + { + "Basic text file, 2 spaces in dir", + "/a b/c.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a--b/c.txt") + }, + }, + { + "Basic text file, 1 space in filename", + "/a/b c.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b-c.txt") + }, + }, + { + "Basic text file, 2 spaces in filename", + "/a/b c.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b--c.txt") + }, + }, + { + "Basic text file, mixed case and spaces, unnormalized", + "/a/Foo BAR.txt", + func(c *qt.C, p *Path) { + pp := p.Unnormalized() + c.Assert(pp, qt.IsNotNil) + c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "Foo BAR") + }, + }, + { + "Basic Markdown file", + "/a/b/c.md", + func(c *qt.C, p *Path) { + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.Type(), qt.Equals, TypeContentSingle) + c.Assert(p.IsContent(), qt.IsTrue) + c.Assert(p.IsLeafBundle(), qt.IsFalse) + c.Assert(p.Name(), qt.Equals, "c.md") + c.Assert(p.Base(), qt.Equals, "/a/b/c") + c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c") + c.Assert(p.Section(), qt.Equals, "a") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c") + c.Assert(p.Path(), qt.Equals, "/a/b/c.md") + c.Assert(p.Dir(), qt.Equals, "/a/b") + c.Assert(p.Container(), qt.Equals, "b") + c.Assert(p.ContainerDir(), qt.Equals, "/a/b") + }, + }, + { + "Content resource", + "/a/b.md", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b.md") + c.Assert(p.Base(), qt.Equals, "/a/b") + c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b") + c.Assert(p.Section(), qt.Equals, "a") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") + + // Reclassify it as a content resource. + ModifyPathBundleTypeResource(p) + c.Assert(p.Type(), qt.Equals, TypeContentResource) + c.Assert(p.IsContent(), qt.IsTrue) + c.Assert(p.Name(), qt.Equals, "b.md") + c.Assert(p.Base(), qt.Equals, "/a/b.md") + }, + }, + { + "No ext", + "/a/b", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b") + c.Assert(p.NameNoExt(), qt.Equals, "b") + c.Assert(p.Base(), qt.Equals, "/a/b") + c.Assert(p.Ext(), qt.Equals, "") + }, + }, + { + "No ext, trailing slash", + "/a/b/", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b") + c.Assert(p.Base(), qt.Equals, "/a/b") + c.Assert(p.Ext(), qt.Equals, "") + }, + }, + { + "Identifiers", + "/a/b.a.b.no.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Name(), qt.Equals, "b.a.b.no.txt") + c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b") + c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"}) + c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt") + c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt") + c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt") + c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt") + c.Assert(p.Ext(), qt.Equals, "txt") + c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b") + }, + }, + { + "Home branch cundle", + "/_index.md", + func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) + c.Assert(p.IsBranchBundle(), qt.IsTrue) + c.Assert(p.IsBundle(), qt.IsTrue) + c.Assert(p.Base(), qt.Equals, "/") + c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo") + c.Assert(p.Path(), qt.Equals, "/_index.md") + c.Assert(p.Container(), qt.Equals, "") + c.Assert(p.ContainerDir(), qt.Equals, "/") + }, + }, + { + "Index content file in root", + "/a/index.md", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a") + c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a") + c.Assert(p.Container(), qt.Equals, "a") + c.Assert(p.Container(), qt.Equals, "a") + c.Assert(p.ContainerDir(), qt.Equals, "") + c.Assert(p.Dir(), qt.Equals, "/a") + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"}) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"}) + c.Assert(p.IsBranchBundle(), qt.IsFalse) + c.Assert(p.IsBundle(), qt.IsTrue) + c.Assert(p.IsLeafBundle(), qt.IsTrue) + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.NameNoExt(), qt.Equals, "index") + c.Assert(p.NameNoIdentifier(), qt.Equals, "index") + c.Assert(p.NameNoLang(), qt.Equals, "index.md") + c.Assert(p.Section(), qt.Equals, "") + }, + }, + { + "Index content file with lang", + "/a/b/index.no.md", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") + c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b") + c.Assert(p.Container(), qt.Equals, "b") + c.Assert(p.ContainerDir(), qt.Equals, "/a") + c.Assert(p.Dir(), qt.Equals, "/a/b") + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"}) + c.Assert(p.IsBranchBundle(), qt.IsFalse) + c.Assert(p.IsBundle(), qt.IsTrue) + c.Assert(p.IsLeafBundle(), qt.IsTrue) + c.Assert(p.Lang(), qt.Equals, "no") + c.Assert(p.NameNoExt(), qt.Equals, "index.no") + c.Assert(p.NameNoIdentifier(), qt.Equals, "index") + c.Assert(p.NameNoLang(), qt.Equals, "index.md") + c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md") + c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md") + c.Assert(p.Section(), qt.Equals, "a") + }, + }, + { + "Index branch content file", + "/a/b/_index.no.md", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b") + c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b") + c.Assert(p.Container(), qt.Equals, "b") + c.Assert(p.ContainerDir(), qt.Equals, "/a") + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md", "no"}) + c.Assert(p.IsBranchBundle(), qt.IsTrue) + c.Assert(p.IsBundle(), qt.IsTrue) + c.Assert(p.IsLeafBundle(), qt.IsFalse) + c.Assert(p.NameNoExt(), qt.Equals, "_index.no") + c.Assert(p.NameNoLang(), qt.Equals, "_index.md") + }, + }, + { + "Index root no slash", + "_index.md", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/") + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.Name(), qt.Equals, "_index.md") + }, + }, + { + "Index root", + "/_index.md", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/") + c.Assert(p.Ext(), qt.Equals, "md") + c.Assert(p.Name(), qt.Equals, "_index.md") + }, + }, + { + "Index first", + "/a/_index.md", + func(c *qt.C, p *Path) { + c.Assert(p.Section(), qt.Equals, "a") + }, + }, + { + "Index text file", + "/a/b/index.no.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b/index.txt") + c.Assert(p.Ext(), qt.Equals, "txt") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"}) + c.Assert(p.IsLeafBundle(), qt.IsFalse) + c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b/index") + }, + }, + { + "Empty", + "", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/") + c.Assert(p.Ext(), qt.Equals, "") + c.Assert(p.Name(), qt.Equals, "") + c.Assert(p.Path(), qt.Equals, "/") + }, + }, + { + "Slash", + "/", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/") + c.Assert(p.Ext(), qt.Equals, "") + c.Assert(p.Name(), qt.Equals, "") + }, + }, + { + "Trim Leading Slash bundle", + "foo/bar/index.no.md", + func(c *qt.C, p *Path) { + c.Assert(p.Path(), qt.Equals, "/foo/bar/index.no.md") + pp := p.TrimLeadingSlash() + c.Assert(pp.Path(), qt.Equals, "foo/bar/index.no.md") + c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar/index.md") + c.Assert(pp.Base(), qt.Equals, "foo/bar") + c.Assert(pp.Dir(), qt.Equals, "foo/bar") + c.Assert(pp.ContainerDir(), qt.Equals, "foo") + c.Assert(pp.Container(), qt.Equals, "bar") + c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar") + }, + }, + { + "Trim Leading Slash file", + "foo/bar.txt", + func(c *qt.C, p *Path) { + c.Assert(p.Path(), qt.Equals, "/foo/bar.txt") + pp := p.TrimLeadingSlash() + c.Assert(pp.Path(), qt.Equals, "foo/bar.txt") + c.Assert(pp.PathNoLang(), qt.Equals, "foo/bar.txt") + c.Assert(pp.Base(), qt.Equals, "foo/bar.txt") + c.Assert(pp.Dir(), qt.Equals, "foo") + c.Assert(pp.ContainerDir(), qt.Equals, "foo") + c.Assert(pp.Container(), qt.Equals, "foo") + c.Assert(pp.BaseNameNoIdentifier(), qt.Equals, "bar") + }, + }, + { + "File separator", + filepath.FromSlash("/a/b/c.txt"), + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/a/b/c.txt") + c.Assert(p.Ext(), qt.Equals, "txt") + c.Assert(p.Name(), qt.Equals, "c.txt") + c.Assert(p.Path(), qt.Equals, "/a/b/c.txt") + }, + }, + { + "Content data file gotmpl", + "/a/b/_content.gotmpl", + func(c *qt.C, p *Path) { + c.Assert(p.Path(), qt.Equals, "/a/b/_content.gotmpl") + c.Assert(p.Ext(), qt.Equals, "gotmpl") + c.Assert(p.IsContentData(), qt.IsTrue) + }, + }, + { + "Content data file yaml", + "/a/b/_content.yaml", + func(c *qt.C, p *Path) { + c.Assert(p.IsContentData(), qt.IsFalse) + }, + }, + } + for _, test := range tests { + c.Run(test.name, func(c *qt.C) { + if test.name != "Home branch cundle" { + // return + } + test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path)) + }) + } +} + +func TestParseLayouts(t *testing.T) { + c := qt.New(t) + + tests := []struct { + name string + path string + assert func(c *qt.C, p *Path) + }{ + { + "Basic", + "/list.html", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/list.html") + c.Assert(p.OutputFormat(), qt.Equals, "html") + }, + }, + { + "Lang", + "/list.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/list.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Kind", + "/section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Kind(), qt.Equals, kinds.KindSection) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/section.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout", + "/list.section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/list.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout multiple", + "/mylayout.list.section.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "mylayout") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Base(), qt.Equals, "/mylayout.html") + c.Assert(p.Lang(), qt.Equals, "no") + }, + }, + { + "Layout shortcode", + "/_shortcodes/myshort.list.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + }, + }, + { + "Layout baseof", + "/baseof.list.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Layout(), qt.Equals, "list") + }, + }, + { + "Lang and output format", + "/list.no.amp.not.html", + func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"}) + c.Assert(p.OutputFormat(), qt.Equals, "amp") + c.Assert(p.Ext(), qt.Equals, "html") + c.Assert(p.Lang(), qt.Equals, "no") + c.Assert(p.Base(), qt.Equals, "/list.html") + }, + }, + { + "Term", + "/term.html", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/term.html") + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"}) + c.Assert(p.PathNoIdentifier(), qt.Equals, "/term") + c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term") + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.Kind(), qt.Equals, "term") + c.Assert(p.OutputFormat(), qt.Equals, "html") + }, + }, + { + "Shortcode with layout", + "/_shortcodes/myshortcode.list.html", + func(c *qt.C, p *Path) { + c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html") + c.Assert(p.Type(), qt.Equals, TypeShortcode) + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list"}) + c.Assert(p.Layout(), qt.Equals, "list") + c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode") + c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list") + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.Kind(), qt.Equals, "") + c.Assert(p.OutputFormat(), qt.Equals, "html") + }, + }, + { + "Sub dir", + "/pages/home.html", + func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"}) + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.Kind(), qt.Equals, "home") + c.Assert(p.OutputFormat(), qt.Equals, "html") + c.Assert(p.Dir(), qt.Equals, "/pages") + }, + }, + { + "Baseof", + "/pages/baseof.list.section.fr.amp.html", + func(c *qt.C, p *Path) { + c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"}) + c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{}) + c.Assert(p.Kind(), qt.Equals, kinds.KindSection) + c.Assert(p.Lang(), qt.Equals, "fr") + c.Assert(p.OutputFormat(), qt.Equals, "amp") + c.Assert(p.Dir(), qt.Equals, "/pages") + c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof") + c.Assert(p.Type(), qt.Equals, TypeBaseof) + c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html") + }, + }, + { + "Markup", + "/_markup/render-link.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeMarkup) + }, + }, + { + "Markup nested", + "/foo/_markup/render-link.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeMarkup) + }, + }, + { + "Shortcode", + "/_shortcodes/myshortcode.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeShortcode) + }, + }, + { + "Shortcode nested", + "/foo/_shortcodes/myshortcode.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeShortcode) + }, + }, + { + "Shortcode nested sub", + "/foo/_shortcodes/foo/myshortcode.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeShortcode) + }, + }, + { + "Partials", + "/_partials/foo.bar", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypePartial) + }, + }, + { + "Shortcode lang in root", + "/_shortcodes/no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeShortcode) + c.Assert(p.Lang(), qt.Equals, "") + c.Assert(p.NameNoIdentifier(), qt.Equals, "no") + }, + }, + { + "Shortcode lang layout", + "/_shortcodes/myshortcode.no.html", + func(c *qt.C, p *Path) { + c.Assert(p.Type(), qt.Equals, TypeShortcode) + c.Assert(p.Lang(), qt.Equals, "no") + c.Assert(p.Layout(), qt.Equals, "") + c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode") + }, + }, + } + + for _, test := range tests { + c.Run(test.name, func(c *qt.C) { + if test.name != "Shortcode lang layout" { + // return + } + test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path)) + }) + } +} + +func TestHasExt(t *testing.T) { + c := qt.New(t) + + c.Assert(HasExt("/a/b/c.txt"), qt.IsTrue) + c.Assert(HasExt("/a/b.c/d.txt"), qt.IsTrue) + c.Assert(HasExt("/a/b/c"), qt.IsFalse) + c.Assert(HasExt("/a/b.c/d"), qt.IsFalse) +} + +func BenchmarkParseIdentity(b *testing.B) { + for i := 0; i < b.N; i++ { + testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css") + } +} diff --git a/common/paths/paths_integration_test.go b/common/paths/paths_integration_test.go new file mode 100644 index 000000000..f5ea3066a --- /dev/null +++ b/common/paths/paths_integration_test.go @@ -0,0 +1,103 @@ +// Copyright 2024 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 paths_test + +import ( + "testing" + + "github.com/gohugoio/hugo/hugolib" +) + +func TestRemovePathAccents(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term"] +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +[languages.fr] +weight = 2 +removePathAccents = true +-- content/διακριτικός.md -- +-- content/διακριτικός.fr.md -- +-- layouts/_default/single.html -- +{{ .Language.Lang }}|Single. +-- layouts/_default/list.html -- +List +` + b := hugolib.Test(t, files) + + b.AssertFileContent("public/en/διακριτικός/index.html", "en|Single") + b.AssertFileContent("public/fr/διακριτικος/index.html", "fr|Single") +} + +func TestDisablePathToLower(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term"] +defaultContentLanguage = "en" +defaultContentLanguageInSubdir = true +[languages] +[languages.en] +weight = 1 +[languages.fr] +weight = 2 +disablePathToLower = true +-- content/MySection/MyPage.md -- +-- content/MySection/MyPage.fr.md -- +-- content/MySection/MyBundle/index.md -- +-- content/MySection/MyBundle/index.fr.md -- +-- layouts/_default/single.html -- +{{ .Language.Lang }}|Single. +-- layouts/_default/list.html -- +{{ .Language.Lang }}|List. +` + b := hugolib.Test(t, files) + + b.AssertFileContent("public/en/mysection/index.html", "en|List") + b.AssertFileContent("public/en/mysection/mypage/index.html", "en|Single") + b.AssertFileContent("public/fr/MySection/index.html", "fr|List") + b.AssertFileContent("public/fr/MySection/MyPage/index.html", "fr|Single") + b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single") + b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single") +} + +func TestIssue13596(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['home','rss','section','sitemap','taxonomy','term'] +-- content/p1/index.md -- +--- +title: p1 +--- +-- content/p1/a.1.txt -- +-- content/p1/a.2.txt -- +-- layouts/all.html -- +{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|") + b.AssertFileExists("public/p1/a.1.txt", true) + b.AssertFileExists("public/p1/a.2.txt", true) // fails +} diff --git a/common/paths/type_string.go b/common/paths/type_string.go new file mode 100644 index 000000000..08fbcc835 --- /dev/null +++ b/common/paths/type_string.go @@ -0,0 +1,32 @@ +// Code generated by "stringer -type Type"; DO NOT EDIT. + +package paths + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TypeFile-0] + _ = x[TypeContentResource-1] + _ = x[TypeContentSingle-2] + _ = x[TypeLeaf-3] + _ = x[TypeBranch-4] + _ = x[TypeContentData-5] + _ = x[TypeMarkup-6] + _ = x[TypeShortcode-7] + _ = x[TypePartial-8] + _ = x[TypeBaseof-9] +} + +const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof" + +var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121} + +func (i Type) String() string { + if i < 0 || i >= Type(len(_Type_index)-1) { + return "Type(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Type_name[_Type_index[i]:_Type_index[i+1]] +} diff --git a/common/paths/url.go b/common/paths/url.go index c538d8f2c..1d1408b51 100644 --- a/common/paths/url.go +++ b/common/paths/url.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -18,6 +18,7 @@ import ( "net/url" "path" "path/filepath" + "runtime" "strings" ) @@ -51,9 +52,10 @@ var pb pathBridge // MakePermalink combines base URL with content path to create full URL paths. // Example -// base: http://spf13.com/ -// path: post/how-i-blog -// result: http://spf13.com/post/how-i-blog +// +// base: http://spf13.com/ +// path: post/how-i-blog +// result: http://spf13.com/post/how-i-blog func MakePermalink(host, plink string) *url.URL { base, err := url.Parse(host) if err != nil { @@ -70,6 +72,8 @@ func MakePermalink(host, plink string) *url.URL { } base.Path = path.Join(base.Path, p.Path) + base.Fragment = p.Fragment + base.RawQuery = p.RawQuery // path.Join will strip off the last /, so put it back if it was there. hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/") @@ -117,17 +121,19 @@ func PrettifyURL(in string) string { // PrettifyURLPath takes a URL path to a content and converts it // to enable pretty URLs. -// /section/name.html becomes /section/name/index.html -// /section/name/ becomes /section/name/index.html -// /section/name/index.html becomes /section/name/index.html +// +// /section/name.html becomes /section/name/index.html +// /section/name/ becomes /section/name/index.html +// /section/name/index.html becomes /section/name/index.html func PrettifyURLPath(in string) string { return prettifyPath(in, pb) } // Uglify does the opposite of PrettifyURLPath(). -// /section/name/index.html becomes /section/name.html -// /section/name/ becomes /section/name.html -// /section/name.html becomes /section/name.html +// +// /section/name/index.html becomes /section/name.html +// /section/name/ becomes /section/name.html +// /section/name.html becomes /section/name.html func Uglify(in string) string { if path.Ext(in) == "" { if len(in) < 2 { @@ -154,11 +160,78 @@ func Uglify(in string) string { return path.Clean(in) } -// UrlToFilename converts the URL s to a filename. -// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned. -func UrlToFilename(s string) (string, bool) { - u, err := url.ParseRequestURI(s) +// URLEscape escapes unicode letters. +func URLEscape(uri string) string { + // escape unicode letters + u, err := url.Parse(uri) + if err != nil { + panic(err) + } + return u.String() +} +// TrimExt trims the extension from a path.. +func TrimExt(in string) string { + return strings.TrimSuffix(in, path.Ext(in)) +} + +// From https://github.com/golang/go/blob/e0c76d95abfc1621259864adb3d101cf6f1f90fc/src/cmd/go/internal/web/url.go#L45 +func UrlFromFilename(filename string) (*url.URL, error) { + if !filepath.IsAbs(filename) { + return nil, fmt.Errorf("filepath must be absolute") + } + + // If filename has a Windows volume name, convert the volume to a host and prefix + // per https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/. + if vol := filepath.VolumeName(filename); vol != "" { + if strings.HasPrefix(vol, `\\`) { + filename = filepath.ToSlash(filename[2:]) + i := strings.IndexByte(filename, '/') + + if i < 0 { + // A degenerate case. + // \\host.example.com (without a share name) + // becomes + // file://host.example.com/ + return &url.URL{ + Scheme: "file", + Host: filename, + Path: "/", + }, nil + } + + // \\host.example.com\Share\path\to\file + // becomes + // file://host.example.com/Share/path/to/file + return &url.URL{ + Scheme: "file", + Host: filename[:i], + Path: filepath.ToSlash(filename[i:]), + }, nil + } + + // C:\path\to\file + // becomes + // file:///C:/path/to/file + return &url.URL{ + Scheme: "file", + Path: "/" + filepath.ToSlash(filename), + }, nil + } + + // /path/to/file + // becomes + // file:///path/to/file + return &url.URL{ + Scheme: "file", + Path: filepath.ToSlash(filename), + }, nil +} + +// UrlStringToFilename converts the URL s to a filename. +// If ParseRequestURI fails, the input is just converted to OS specific slashes and returned. +func UrlStringToFilename(s string) (string, bool) { + u, err := url.ParseRequestURI(s) if err != nil { return filepath.FromSlash(s), false } @@ -167,15 +240,34 @@ func UrlToFilename(s string) (string, bool) { if p == "" { p, _ = url.QueryUnescape(u.Opaque) - return filepath.FromSlash(p), true + return filepath.FromSlash(p), false + } + + if runtime.GOOS != "windows" { + return p, true + } + + if len(p) == 0 || p[0] != '/' { + return filepath.FromSlash(p), false } p = filepath.FromSlash(p) - if u.Host != "" { - // C:\data\file.txt - p = strings.ToUpper(u.Host) + ":" + p + if len(u.Host) == 1 { + // file://c/Users/... + return strings.ToUpper(u.Host) + ":" + p, true } - return p, true + if u.Host != "" && u.Host != "localhost" { + if filepath.VolumeName(u.Host) != "" { + return "", false + } + return `\\` + u.Host + p, true + } + + if vol := filepath.VolumeName(p[1:]); vol == "" || strings.HasPrefix(vol, `\\`) { + return "", false + } + + return p[1:], true } diff --git a/common/paths/url_test.go b/common/paths/url_test.go index 4e5f73053..5a9233c26 100644 --- a/common/paths/url_test.go +++ b/common/paths/url_test.go @@ -31,6 +31,7 @@ func TestMakePermalink(t *testing.T) { {"http://abc.com", "bar", "http://abc.com/bar"}, {"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"}, {"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"}, + {"http://abc.com/foo", "post/bar?a=b#c", "http://abc.com/foo/post/bar?a=b#c"}, } for i, d := range data { diff --git a/common/predicate/predicate.go b/common/predicate/predicate.go new file mode 100644 index 000000000..f71536474 --- /dev/null +++ b/common/predicate/predicate.go @@ -0,0 +1,78 @@ +// Copyright 2024 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 predicate + +// P is a predicate function that tests whether a value of type T satisfies some condition. +type P[T any] func(T) bool + +// And returns a predicate that is a short-circuiting logical AND of this and the given predicates. +func (p P[T]) And(ps ...P[T]) P[T] { + return func(v T) bool { + for _, pp := range ps { + if !pp(v) { + return false + } + } + if p == nil { + return true + } + return p(v) + } +} + +// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates. +func (p P[T]) Or(ps ...P[T]) P[T] { + return func(v T) bool { + for _, pp := range ps { + if pp(v) { + return true + } + } + if p == nil { + return false + } + return p(v) + } +} + +// Negate returns a predicate that is a logical negation of this predicate. +func (p P[T]) Negate() P[T] { + return func(v T) bool { + return !p(v) + } +} + +// Filter returns a new slice holding only the elements of s that satisfy p. +// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length. +func (p P[T]) Filter(s []T) []T { + var n int + for _, v := range s { + if p(v) { + s[n] = v + n++ + } + } + return s[:n] +} + +// FilterCopy returns a new slice holding only the elements of s that satisfy p. +func (p P[T]) FilterCopy(s []T) []T { + var result []T + for _, v := range s { + if p(v) { + result = append(result, v) + } + } + return result +} diff --git a/common/predicate/predicate_test.go b/common/predicate/predicate_test.go new file mode 100644 index 000000000..1e1ec004b --- /dev/null +++ b/common/predicate/predicate_test.go @@ -0,0 +1,83 @@ +// Copyright 2024 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 predicate_test + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/predicate" +) + +func TestAdd(t *testing.T) { + c := qt.New(t) + + var p predicate.P[int] = intP1 + + c.Assert(p(1), qt.IsTrue) + c.Assert(p(2), qt.IsFalse) + + neg := p.Negate() + c.Assert(neg(1), qt.IsFalse) + c.Assert(neg(2), qt.IsTrue) + + and := p.And(intP2) + c.Assert(and(1), qt.IsFalse) + c.Assert(and(2), qt.IsFalse) + c.Assert(and(10), qt.IsTrue) + + or := p.Or(intP2) + c.Assert(or(1), qt.IsTrue) + c.Assert(or(2), qt.IsTrue) + c.Assert(or(10), qt.IsTrue) + c.Assert(or(11), qt.IsFalse) +} + +func TestFilter(t *testing.T) { + c := qt.New(t) + + var p predicate.P[int] = intP1 + p = p.Or(intP2) + + ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2} + + c.Assert(p.Filter(ints), qt.DeepEquals, []int{1, 2, 1, 2}) + c.Assert(ints, qt.DeepEquals, []int{1, 2, 1, 2, 1, 6, 7, 8, 2}) +} + +func TestFilterCopy(t *testing.T) { + c := qt.New(t) + + var p predicate.P[int] = intP1 + p = p.Or(intP2) + + ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2} + + c.Assert(p.FilterCopy(ints), qt.DeepEquals, []int{1, 2, 1, 2}) + c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2}) +} + +var intP1 = func(i int) bool { + if i == 10 { + return true + } + return i == 1 +} + +var intP2 = func(i int) bool { + if i == 10 { + return true + } + return i == 2 +} diff --git a/common/rungroup/rungroup.go b/common/rungroup/rungroup.go new file mode 100644 index 000000000..80a730ca9 --- /dev/null +++ b/common/rungroup/rungroup.go @@ -0,0 +1,93 @@ +// Copyright 2024 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 rungroup + +import ( + "context" + + "golang.org/x/sync/errgroup" +) + +// Group is a group of workers that can be used to enqueue work and wait for +// them to finish. +type Group[T any] interface { + Enqueue(T) error + Wait() error +} + +type runGroup[T any] struct { + ctx context.Context + g *errgroup.Group + ch chan T +} + +// Config is the configuration for a new Group. +type Config[T any] struct { + NumWorkers int + Handle func(context.Context, T) error +} + +// Run creates a new Group with the given configuration. +func Run[T any](ctx context.Context, cfg Config[T]) Group[T] { + if cfg.NumWorkers <= 0 { + cfg.NumWorkers = 1 + } + if cfg.Handle == nil { + panic("Handle must be set") + } + + g, ctx := errgroup.WithContext(ctx) + // Buffered for performance. + ch := make(chan T, cfg.NumWorkers) + + for range cfg.NumWorkers { + g.Go(func() error { + for { + select { + case <-ctx.Done(): + return nil + case v, ok := <-ch: + if !ok { + return nil + } + if err := cfg.Handle(ctx, v); err != nil { + return err + } + } + } + }) + } + + return &runGroup[T]{ + ctx: ctx, + g: g, + ch: ch, + } +} + +// Enqueue enqueues a new item to be handled by the workers. +func (r *runGroup[T]) Enqueue(t T) error { + select { + case <-r.ctx.Done(): + return nil + case r.ch <- t: + } + return nil +} + +// Wait waits for all workers to finish and returns the first error. +func (r *runGroup[T]) Wait() error { + close(r.ch) + return r.g.Wait() +} diff --git a/common/rungroup/rungroup_test.go b/common/rungroup/rungroup_test.go new file mode 100644 index 000000000..ac902079e --- /dev/null +++ b/common/rungroup/rungroup_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 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 rungroup + +import ( + "context" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestNew(t *testing.T) { + c := qt.New(t) + + var result int + adder := func(ctx context.Context, i int) error { + result += i + return nil + } + + g := Run[int]( + context.Background(), + Config[int]{ + Handle: adder, + }, + ) + + c.Assert(g, qt.IsNotNil) + g.Enqueue(32) + g.Enqueue(33) + c.Assert(g.Wait(), qt.IsNil) + c.Assert(result, qt.Equals, 65) +} diff --git a/common/tasks/tasks.go b/common/tasks/tasks.go new file mode 100644 index 000000000..3f8a754e9 --- /dev/null +++ b/common/tasks/tasks.go @@ -0,0 +1,150 @@ +// Copyright 2024 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 tasks + +import ( + "sync" + "time" +) + +// RunEvery runs a function at intervals defined by the function itself. +// Functions can be added and removed while running. +type RunEvery struct { + // Any error returned from the function will be passed to this function. + HandleError func(string, error) + + // If set, the function will be run immediately. + RunImmediately bool + + // The named functions to run. + funcs map[string]*Func + + mu sync.Mutex + started bool + closed bool + quit chan struct{} +} + +type Func struct { + // The shortest interval between each run. + IntervalLow time.Duration + + // The longest interval between each run. + IntervalHigh time.Duration + + // The function to run. + F func(interval time.Duration) (time.Duration, error) + + interval time.Duration + last time.Time +} + +func (r *RunEvery) Start() error { + if r.started { + return nil + } + + r.started = true + r.quit = make(chan struct{}) + + go func() { + if r.RunImmediately { + r.run() + } + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-r.quit: + return + case <-ticker.C: + r.run() + } + } + }() + + return nil +} + +// Close stops the RunEvery from running. +func (r *RunEvery) Close() error { + if r.closed { + return nil + } + r.closed = true + if r.quit != nil { + close(r.quit) + } + return nil +} + +// Add adds a function to the RunEvery. +func (r *RunEvery) Add(name string, f Func) { + r.mu.Lock() + defer r.mu.Unlock() + if r.funcs == nil { + r.funcs = make(map[string]*Func) + } + if f.IntervalLow == 0 { + f.IntervalLow = 500 * time.Millisecond + } + if f.IntervalHigh <= f.IntervalLow { + f.IntervalHigh = 20 * time.Second + } + + start := max(f.IntervalHigh/3, f.IntervalLow) + f.interval = start + f.last = time.Now() + + r.funcs[name] = &f +} + +// Remove removes a function from the RunEvery. +func (r *RunEvery) Remove(name string) { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.funcs, name) +} + +// Has returns whether the RunEvery has a function with the given name. +func (r *RunEvery) Has(name string) bool { + r.mu.Lock() + defer r.mu.Unlock() + _, found := r.funcs[name] + return found +} + +func (r *RunEvery) run() { + r.mu.Lock() + defer r.mu.Unlock() + for name, f := range r.funcs { + if time.Now().Before(f.last.Add(f.interval)) { + continue + } + f.last = time.Now() + interval, err := f.F(f.interval) + if err != nil && r.HandleError != nil { + r.HandleError(name, err) + } + + if interval < f.IntervalLow { + interval = f.IntervalLow + } + + if interval > f.IntervalHigh { + interval = f.IntervalHigh + } + f.interval = interval + } +} diff --git a/common/terminal/colors.go b/common/terminal/colors.go index c4a78291e..fef6efce8 100644 --- a/common/terminal/colors.go +++ b/common/terminal/colors.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -17,7 +17,6 @@ package terminal import ( "fmt" "os" - "runtime" "strings" isatty "github.com/mattn/go-isatty" @@ -41,10 +40,6 @@ func PrintANSIColors(f *os.File) bool { // IsTerminal return true if the file descriptor is terminal and the TERM // environment variable isn't a dumb one. func IsTerminal(f *os.File) bool { - if runtime.GOOS == "windows" { - return false - } - fd := f.Fd() return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)) } diff --git a/common/text/transform_test.go b/common/text/transform_test.go index 41447715f..74bb37783 100644 --- a/common/text/transform_test.go +++ b/common/text/transform_test.go @@ -57,7 +57,6 @@ line 3` c := qt.New(t) c.Assert(collected, qt.DeepEquals, []string{"line 1\n", "line 2\n", "\n", "line 3"}) - } func BenchmarkVisitLinesAfter(b *testing.B) { @@ -68,9 +67,6 @@ func BenchmarkVisitLinesAfter(b *testing.B) { for i := 0; i < b.N; i++ { VisitLinesAfter(lines, func(s string) { - }) - } - } diff --git a/common/types/closer.go b/common/types/closer.go new file mode 100644 index 000000000..9f8875a8a --- /dev/null +++ b/common/types/closer.go @@ -0,0 +1,54 @@ +// Copyright 2024 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 types + +import "sync" + +type Closer interface { + Close() error +} + +// CloserFunc is a convenience type to create a Closer from a function. +type CloserFunc func() error + +func (f CloserFunc) Close() error { + return f() +} + +type CloseAdder interface { + Add(Closer) +} + +type Closers struct { + mu sync.Mutex + cs []Closer +} + +func (cs *Closers) Add(c Closer) { + cs.mu.Lock() + defer cs.mu.Unlock() + cs.cs = append(cs.cs, c) +} + +func (cs *Closers) Close() error { + cs.mu.Lock() + defer cs.mu.Unlock() + for _, c := range cs.cs { + c.Close() + } + + cs.cs = cs.cs[:0] + + return nil +} diff --git a/common/types/convert.go b/common/types/convert.go index fbeab5b91..6b1750376 100644 --- a/common/types/convert.go +++ b/common/types/convert.go @@ -69,7 +69,7 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) { switch vv.Kind() { case reflect.Slice, reflect.Array: result = make([]string, vv.Len()) - for i := 0; i < vv.Len(); i++ { + for i := range vv.Len() { s, err := cast.ToStringE(vv.Index(i).Interface()) if err != nil { return nil, err @@ -80,7 +80,6 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) { default: return nil, fmt.Errorf("failed to convert %T to a string slice", v) } - } // TypeToString converts v to a string if it's a valid string type. diff --git a/common/types/convert_test.go b/common/types/convert_test.go index 215117441..13059285d 100644 --- a/common/types/convert_test.go +++ b/common/types/convert_test.go @@ -45,5 +45,4 @@ func TestToDuration(t *testing.T) { c.Assert(ToDuration("200"), qt.Equals, 200*time.Millisecond) c.Assert(ToDuration("4m"), qt.Equals, 4*time.Minute) c.Assert(ToDuration("asdfadf"), qt.Equals, time.Duration(0)) - } diff --git a/common/types/css/csstypes.go b/common/types/css/csstypes.go index a31df00e7..061acfe64 100644 --- a/common/types/css/csstypes.go +++ b/common/types/css/csstypes.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. diff --git a/common/types/evictingqueue.go b/common/types/evictingqueue.go index 884762426..a335be3b2 100644 --- a/common/types/evictingqueue.go +++ b/common/types/evictingqueue.go @@ -15,57 +15,72 @@ package types import ( + "slices" "sync" ) -// EvictingStringQueue is a queue which automatically evicts elements from the head of +// EvictingQueue is a queue which automatically evicts elements from the head of // the queue when attempting to add new elements onto the queue and it is full. // This queue orders elements LIFO (last-in-first-out). It throws away duplicates. -// Note: This queue currently does not contain any remove (poll etc.) methods. -type EvictingStringQueue struct { +type EvictingQueue[T comparable] struct { size int - vals []string - set map[string]bool + vals []T + set map[T]bool mu sync.Mutex + zero T } -// NewEvictingStringQueue creates a new queue with the given size. -func NewEvictingStringQueue(size int) *EvictingStringQueue { - return &EvictingStringQueue{size: size, set: make(map[string]bool)} +// NewEvictingQueue creates a new queue with the given size. +func NewEvictingQueue[T comparable](size int) *EvictingQueue[T] { + return &EvictingQueue[T]{size: size, set: make(map[T]bool)} } // Add adds a new string to the tail of the queue if it's not already there. -func (q *EvictingStringQueue) Add(v string) { +func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] { q.mu.Lock() if q.set[v] { q.mu.Unlock() - return + return q } if len(q.set) == q.size { // Full delete(q.set, q.vals[0]) - q.vals = append(q.vals[:0], q.vals[1:]...) + q.vals = slices.Delete(q.vals, 0, 1) } q.set[v] = true q.vals = append(q.vals, v) q.mu.Unlock() + + return q +} + +func (q *EvictingQueue[T]) Len() int { + if q == nil { + return 0 + } + q.mu.Lock() + defer q.mu.Unlock() + return len(q.vals) } // Contains returns whether the queue contains v. -func (q *EvictingStringQueue) Contains(v string) bool { +func (q *EvictingQueue[T]) Contains(v T) bool { + if q == nil { + return false + } q.mu.Lock() defer q.mu.Unlock() return q.set[v] } // Peek looks at the last element added to the queue. -func (q *EvictingStringQueue) Peek() string { +func (q *EvictingQueue[T]) Peek() T { q.mu.Lock() l := len(q.vals) if l == 0 { q.mu.Unlock() - return "" + return q.zero } elem := q.vals[l-1] q.mu.Unlock() @@ -73,9 +88,12 @@ func (q *EvictingStringQueue) Peek() string { } // PeekAll looks at all the elements in the queue, with the newest first. -func (q *EvictingStringQueue) PeekAll() []string { +func (q *EvictingQueue[T]) PeekAll() []T { + if q == nil { + return nil + } q.mu.Lock() - vals := make([]string, len(q.vals)) + vals := make([]T, len(q.vals)) copy(vals, q.vals) q.mu.Unlock() for i, j := 0, len(vals)-1; i < j; i, j = i+1, j-1 { @@ -85,9 +103,9 @@ func (q *EvictingStringQueue) PeekAll() []string { } // PeekAllSet returns PeekAll as a set. -func (q *EvictingStringQueue) PeekAllSet() map[string]bool { +func (q *EvictingQueue[T]) PeekAllSet() map[T]bool { all := q.PeekAll() - set := make(map[string]bool) + set := make(map[T]bool) for _, v := range all { set[v] = true } diff --git a/common/types/evictingqueue_test.go b/common/types/evictingqueue_test.go index 7489ba88d..b93243f3c 100644 --- a/common/types/evictingqueue_test.go +++ b/common/types/evictingqueue_test.go @@ -23,7 +23,7 @@ import ( func TestEvictingStringQueue(t *testing.T) { c := qt.New(t) - queue := NewEvictingStringQueue(3) + queue := NewEvictingQueue[string](3) c.Assert(queue.Peek(), qt.Equals, "") queue.Add("a") @@ -53,9 +53,9 @@ func TestEvictingStringQueueConcurrent(t *testing.T) { var wg sync.WaitGroup val := "someval" - queue := NewEvictingStringQueue(3) + queue := NewEvictingQueue[string](3) - for j := 0; j < 100; j++ { + for range 100 { wg.Add(1) go func() { defer wg.Done() diff --git a/common/types/hstring/stringtypes.go b/common/types/hstring/stringtypes.go index 601218e0e..53ce2068f 100644 --- a/common/types/hstring/stringtypes.go +++ b/common/types/hstring/stringtypes.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -13,8 +13,24 @@ package hstring -type RenderedString string +import ( + "html/template" -func (s RenderedString) String() string { + "github.com/gohugoio/hugo/common/types" +) + +var _ types.PrintableValueProvider = HTML("") + +// HTML is a string that represents rendered HTML. +// When printed in templates it will be rendered as template.HTML and considered safe so no need to pipe it into `safeHTML`. +// This type was introduced as a wasy to prevent a common case of inifinite recursion in the template rendering +// when the `linkify` option is enabled with a common (wrong) construct like `{{ .Text | .Page.RenderString }}` in a hook template. +type HTML string + +func (s HTML) String() string { return string(s) } + +func (s HTML) PrintableValue() any { + return template.HTML(s) +} diff --git a/common/types/hstring/stringtypes_test.go b/common/types/hstring/stringtypes_test.go index 8fa1c9760..05e2c22b9 100644 --- a/common/types/hstring/stringtypes_test.go +++ b/common/types/hstring/stringtypes_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -25,6 +25,6 @@ func TestRenderedString(t *testing.T) { c := qt.New(t) // Validate that it will behave like a string in Hugo settings. - c.Assert(cast.ToString(RenderedString("Hugo")), qt.Equals, "Hugo") - c.Assert(template.HTML(RenderedString("Hugo")), qt.Equals, template.HTML("Hugo")) + c.Assert(cast.ToString(HTML("Hugo")), qt.Equals, "Hugo") + c.Assert(template.HTML(HTML("Hugo")), qt.Equals, template.HTML("Hugo")) } diff --git a/common/types/types.go b/common/types/types.go index c36c51b3e..7e94c1eea 100644 --- a/common/types/types.go +++ b/common/types/types.go @@ -28,6 +28,16 @@ type RLocker interface { RUnlock() } +type Locker interface { + Lock() + Unlock() +} + +type RWLocker interface { + RLocker + Locker +} + // KeyValue is a interface{} tuple. type KeyValue struct { Key any @@ -59,7 +69,7 @@ func (k KeyValues) String() string { // KeyValues struct. func NewKeyValuesStrings(key string, values ...string) KeyValues { iv := make([]any, len(values)) - for i := 0; i < len(values); i++ { + for i := range values { iv[i] = values[i] } return KeyValues{Key: key, Values: iv} @@ -92,5 +102,44 @@ type DevMarker interface { DevOnly() } +// Unwrapper is implemented by types that can unwrap themselves. +type Unwrapper interface { + // Unwrapv is for internal use only. + // It got its slightly odd name to prevent collisions with user types. + Unwrapv() any +} + +// Unwrap returns the underlying value of v if it implements Unwrapper, otherwise v is returned. +func Unwrapv(v any) any { + if u, ok := v.(Unwrapper); ok { + return u.Unwrapv() + } + return v +} + +// LowHigh represents a byte or slice boundary. +type LowHigh[S ~[]byte | string] struct { + Low int + High int +} + +func (l LowHigh[S]) IsZero() bool { + return l.Low < 0 || (l.Low == 0 && l.High == 0) +} + +func (l LowHigh[S]) Value(source S) S { + return source[l.Low:l.High] +} + // This is only used for debugging purposes. var InvocationCounter atomic.Int64 + +// NewTrue returns a pointer to b. +func NewBool(b bool) *bool { + return &b +} + +// PrintableValueProvider is implemented by types that can provide a printable value. +type PrintableValueProvider interface { + PrintableValue() any +} diff --git a/common/types/types_test.go b/common/types/types_test.go index 6f13ae834..795733047 100644 --- a/common/types/types_test.go +++ b/common/types/types_test.go @@ -27,3 +27,25 @@ func TestKeyValues(t *testing.T) { c.Assert(kv.KeyString(), qt.Equals, "key") c.Assert(kv.Values, qt.DeepEquals, []any{"a1", "a2"}) } + +func TestLowHigh(t *testing.T) { + c := qt.New(t) + + lh := LowHigh[string]{ + Low: 2, + High: 10, + } + + s := "abcdefghijklmnopqrstuvwxyz" + c.Assert(lh.IsZero(), qt.IsFalse) + c.Assert(lh.Value(s), qt.Equals, "cdefghij") + + lhb := LowHigh[[]byte]{ + Low: 2, + High: 10, + } + + sb := []byte(s) + c.Assert(lhb.IsZero(), qt.IsFalse) + c.Assert(lhb.Value(sb), qt.DeepEquals, []byte("cdefghij")) +} diff --git a/common/urls/baseURL.go b/common/urls/baseURL.go index df26730ec..2958a2a04 100644 --- a/common/urls/baseURL.go +++ b/common/urls/baseURL.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -23,10 +23,12 @@ import ( // A BaseURL in Hugo is normally on the form scheme://path, but the // form scheme: is also valid (mailto:hugo@rules.com). type BaseURL struct { - url *url.URL - WithPath string - WithoutPath string - BasePath string + url *url.URL + WithPath string + WithPathNoTrailingSlash string + WithoutPath string + BasePath string + BasePathNoTrailingSlash string } func (b BaseURL) String() string { @@ -92,19 +94,19 @@ func NewBaseURLFromString(b string) (BaseURL, error) { return BaseURL{}, err } return newBaseURLFromURL(u) - } func newBaseURLFromURL(u *url.URL) (BaseURL, error) { - baseURL := BaseURL{url: u, WithPath: u.String()} - var baseURLNoPath = baseURL.URL() + // A baseURL should always have a trailing slash, see #11669. + if !strings.HasSuffix(u.Path, "/") { + u.Path += "/" + } + baseURL := BaseURL{url: u, WithPath: u.String(), WithPathNoTrailingSlash: strings.TrimSuffix(u.String(), "/")} + baseURLNoPath := baseURL.URL() baseURLNoPath.Path = "" baseURL.WithoutPath = baseURLNoPath.String() - - basePath := u.Path - if basePath != "" && basePath != "/" { - baseURL.BasePath = basePath - } + baseURL.BasePath = u.Path + baseURL.BasePathNoTrailingSlash = strings.TrimSuffix(u.Path, "/") return baseURL, nil } diff --git a/common/urls/baseURL_test.go b/common/urls/baseURL_test.go index 95dc73339..ba337aac8 100644 --- a/common/urls/baseURL_test.go +++ b/common/urls/baseURL_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -21,17 +21,24 @@ import ( func TestBaseURL(t *testing.T) { c := qt.New(t) - b, err := NewBaseURLFromString("http://example.com") + + b, err := NewBaseURLFromString("http://example.com/") c.Assert(err, qt.IsNil) - c.Assert(b.String(), qt.Equals, "http://example.com") + c.Assert(b.String(), qt.Equals, "http://example.com/") + + b, err = NewBaseURLFromString("http://example.com") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "http://example.com/") + c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com") + c.Assert(b.BasePath, qt.Equals, "/") p, err := b.WithProtocol("webcal://") c.Assert(err, qt.IsNil) - c.Assert(p.String(), qt.Equals, "webcal://example.com") + c.Assert(p.String(), qt.Equals, "webcal://example.com/") p, err = b.WithProtocol("webcal") c.Assert(err, qt.IsNil) - c.Assert(p.String(), qt.Equals, "webcal://example.com") + c.Assert(p.String(), qt.Equals, "webcal://example.com/") _, err = b.WithProtocol("mailto:") c.Assert(err, qt.Not(qt.IsNil)) @@ -57,11 +64,18 @@ func TestBaseURL(t *testing.T) { b, err = NewBaseURLFromString("") c.Assert(err, qt.IsNil) - c.Assert(b.String(), qt.Equals, "") + c.Assert(b.String(), qt.Equals, "/") // BaseURL with sub path b, err = NewBaseURLFromString("http://example.com/sub") c.Assert(err, qt.IsNil) - c.Assert(b.String(), qt.Equals, "http://example.com/sub") + c.Assert(b.String(), qt.Equals, "http://example.com/sub/") + c.Assert(b.WithPathNoTrailingSlash, qt.Equals, "http://example.com/sub") + c.Assert(b.BasePath, qt.Equals, "/sub/") + c.Assert(b.BasePathNoTrailingSlash, qt.Equals, "/sub") + + b, err = NewBaseURLFromString("http://example.com/sub/") + c.Assert(err, qt.IsNil) + c.Assert(b.String(), qt.Equals, "http://example.com/sub/") c.Assert(b.HostURL(), qt.Equals, "http://example.com") } diff --git a/compare/compare.go b/compare/compare.go index 67bb1c125..fd15bd087 100644 --- a/compare/compare.go +++ b/compare/compare.go @@ -52,3 +52,16 @@ func Eq(v1, v2 any) bool { return v1 == v2 } + +// ProbablyEq returns whether v1 is probably equal to v2. +func ProbablyEq(v1, v2 any) bool { + if Eq(v1, v2) { + return true + } + + if peqer, ok := v1.(ProbablyEqer); ok { + return peqer.ProbablyEq(v2) + } + + return false +} diff --git a/compare/compare_strings_test.go b/compare/compare_strings_test.go index a73091fc6..1a5bb0b1a 100644 --- a/compare/compare_strings_test.go +++ b/compare/compare_strings_test.go @@ -79,5 +79,4 @@ func BenchmarkStringSort(b *testing.B) { }) } }) - } diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index ade7ea1be..0db0be1d8 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -27,15 +27,20 @@ import ( "time" "github.com/gohugoio/hugo/cache/filecache" + "github.com/gohugoio/hugo/cache/httpcache" + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/services" - "github.com/gohugoio/hugo/deploy" + "github.com/gohugoio/hugo/deploy/deployconfig" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugolib/segments" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/media" @@ -45,6 +50,7 @@ import ( "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/resources/images" + "github.com/gohugoio/hugo/resources/kinds" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/spf13/afero" @@ -57,12 +63,12 @@ type InternalConfig struct { // Server mode? Running bool - Quiet bool - Verbose bool - Clock string - Watch bool - DisableLiveReload bool - LiveReloadPort int + Quiet bool + Verbose bool + Clock string + Watch bool + FastRenderMode bool + LiveReloadPort int } // All non-params config keys for language. @@ -76,7 +82,7 @@ func init() { } configLanguageKeys = make(map[string]bool) addKeys := func(v reflect.Value) { - for i := 0; i < v.NumField(); i++ { + for i := range v.NumField() { name := strings.ToLower(v.Type().Field(i).Name) if skip[name] { continue @@ -99,9 +105,11 @@ type Config struct { RootConfig // Author information. + // Deprecated: Use taxonomies instead. Author map[string]any // Social links. + // Deprecated: Use .Site.Params instead. Social map[string]string // The build configuration section contains build-related configuration options. @@ -112,10 +120,17 @@ type Config struct { // {"identifiers": ["caches"] } Caches filecache.Configs `mapstructure:"-"` + // The httpcache configuration section contains HTTP-cache-related configuration options. + // {"identifiers": ["httpcache"] } + HTTPCache httpcache.Config `mapstructure:"-"` + // The markup configuration section contains markup-related configuration options. // {"identifiers": ["markup"] } Markup markup_config.Config `mapstructure:"-"` + // ContentTypes are the media types that's considered content in Hugo. + ContentTypes *config.ConfigNamespace[map[string]media.ContentTypeConfig, media.ContentTypes] `mapstructure:"-"` + // The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type. // {"identifiers": ["mediatypes"], "refs": ["types:media:type"] } MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"` @@ -131,14 +146,17 @@ type Config struct { // The cascade configuration section contains the top level front matter cascade configuration options, // a slice of page matcher and params to apply to those pages. - Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"` + Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"` + + // The segments defines segments for the site. Used for partial/segmented builds. + Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"` // Menu configuration. // {"refs": ["config:languages:menus"] } Menus *config.ConfigNamespace[map[string]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"` - // The deployment configuration section contains for hugo deploy. - Deployment deploy.DeployConfig `mapstructure:"-"` + // The deployment configuration section contains for hugo deployconfig. + Deployment deployconfig.DeployConfig `mapstructure:"-"` // Module configuration. Module modules.Config `mapstructure:"-"` @@ -150,7 +168,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:"-"` @@ -164,6 +182,12 @@ type Config struct { // Server configuration. Server config.Server `mapstructure:"-"` + // Pagination configuration. + Pagination config.Pagination `mapstructure:"-"` + + // Page configuration. + Page config.PageConfig `mapstructure:"-"` + // Privacy configuration. Privacy privacy.Config `mapstructure:"-"` @@ -191,6 +215,22 @@ type configCompiler interface { func (c Config) cloneForLang() *Config { x := c x.C = nil + copyStringSlice := func(in []string) []string { + if in == nil { + return nil + } + out := make([]string, len(in)) + copy(out, in) + return out + } + + // Copy all the slices to avoid sharing. + x.DisableKinds = copyStringSlice(x.DisableKinds) + x.DisableLanguages = copyStringSlice(x.DisableLanguages) + x.MainSections = copyStringSlice(x.MainSections) + x.IgnoreLogs = copyStringSlice(x.IgnoreLogs) + x.IgnoreFiles = copyStringSlice(x.IgnoreFiles) + x.Theme = copyStringSlice(x.Theme) // Collapse all static dirs to one. x.StaticDir = x.staticDirs() @@ -224,9 +264,14 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { disabledKinds := make(map[string]bool) for _, kind := range c.DisableKinds { kind = strings.ToLower(kind) - if kind == "taxonomyterm" { + if newKind := kinds.IsDeprecatedAndReplacedWith(kind); newKind != "" { + logger.Deprecatef(false, "Kind %q used in disableKinds is deprecated, use %q instead.", kind, newKind) // Legacy config. - kind = "term" + kind = newKind + } + if kinds.GetKindAny(kind) == "" { + logger.Warnf("Unknown kind %q in disableKinds configuration.", kind) + continue } disabledKinds[kind] = true } @@ -234,9 +279,17 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { isRssDisabled := disabledKinds["rss"] outputFormats := c.OutputFormats.Config for kind, formats := range c.Outputs { + if newKind := kinds.IsDeprecatedAndReplacedWith(kind); newKind != "" { + logger.Deprecatef(false, "Kind %q used in outputs configuration is deprecated, use %q instead.", kind, newKind) + kind = newKind + } if disabledKinds[kind] { continue } + if kinds.GetKindAny(kind) == "" { + logger.Warnf("Unknown kind %q in outputs configuration.", kind) + continue + } for _, format := range formats { if isRssDisabled && format == "rss" { // Legacy config. @@ -251,17 +304,42 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { } } - disabledLangs := make(map[string]bool) - for _, lang := range c.DisableLanguages { - if lang == c.DefaultContentLanguage { - return fmt.Errorf("cannot disable default content language %q", lang) + defaultOutputFormat := outputFormats[0] + c.DefaultOutputFormat = strings.ToLower(c.DefaultOutputFormat) + if c.DefaultOutputFormat != "" { + f, found := outputFormats.GetByName(c.DefaultOutputFormat) + if !found { + return fmt.Errorf("unknown default output format %q", c.DefaultOutputFormat) } - disabledLangs[lang] = true + defaultOutputFormat = f + } else { + c.DefaultOutputFormat = defaultOutputFormat.Name } - ignoredErrors := make(map[string]bool) - for _, err := range c.IgnoreErrors { - ignoredErrors[strings.ToLower(err)] = true + disabledLangs := make(map[string]bool) + for _, lang := range c.DisableLanguages { + disabledLangs[lang] = true + } + for lang, language := range c.Languages { + if !language.Disabled && disabledLangs[lang] { + language.Disabled = true + c.Languages[lang] = language + } + if language.Disabled { + disabledLangs[lang] = true + if lang == c.DefaultContentLanguage { + return fmt.Errorf("cannot disable default content language %q", lang) + } + } + } + + for i, s := range c.IgnoreLogs { + c.IgnoreLogs[i] = strings.ToLower(s) + } + + ignoredLogIDs := make(map[string]bool) + for _, err := range c.IgnoreLogs { + ignoredLogIDs[err] = true } baseURL, err := urls.NewBaseURLFromString(c.BaseURL) @@ -311,20 +389,70 @@ func (c *Config) CompileConfig(logger loggers.Logger) error { } } + httpCache, err := c.HTTPCache.Compile() + if err != nil { + return err + } + + // Legacy paginate values. + if c.Paginate != 0 { + hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger()) + c.Pagination.PagerSize = c.Paginate + } + + if c.PaginatePath != "" { + hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger()) + c.Pagination.Path = c.PaginatePath + } + + // Legacy privacy values. + if c.Privacy.Twitter.Disable { + hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger()) + c.Privacy.X.Disable = c.Privacy.Twitter.Disable + } + + if c.Privacy.Twitter.EnableDNT { + hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger()) + c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT + } + + if c.Privacy.Twitter.Simple { + hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger()) + c.Privacy.X.Simple = c.Privacy.Twitter.Simple + } + + // Legacy services values. + if c.Services.Twitter.DisableInlineCSS { + hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger()) + c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS + } + + // Legacy permalink tokens + vs := fmt.Sprintf("%v", c.Permalinks) + if strings.Contains(vs, ":filename") { + hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger()) + } + if strings.Contains(vs, ":slugorfilename") { + hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger()) + } + c.C = &ConfigCompiled{ - Timeout: timeout, - BaseURL: baseURL, - BaseURLLiveReload: baseURL, - DisabledKinds: disabledKinds, - DisabledLanguages: disabledLangs, - IgnoredErrors: ignoredErrors, - KindOutputFormats: kindOutputFormats, - CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle), - IsUglyURLSection: isUglyURL, - IgnoreFile: ignoreFile, - MainSections: c.MainSections, - Clock: clock, - transientErr: transientErr, + Timeout: timeout, + BaseURL: baseURL, + BaseURLLiveReload: baseURL, + DisabledKinds: disabledKinds, + DisabledLanguages: disabledLangs, + IgnoredLogs: ignoredLogIDs, + KindOutputFormats: kindOutputFormats, + DefaultOutputFormat: defaultOutputFormat, + CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle), + IsUglyURLSection: isUglyURL, + IgnoreFile: ignoreFile, + SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...), + MainSections: c.MainSections, + Clock: clock, + HTTPCache: httpCache, + transientErr: transientErr, } for _, s := range allDecoderSetups { @@ -348,18 +476,22 @@ func (c *Config) IsLangDisabled(lang string) bool { // ConfigCompiled holds values and functions that are derived from the config. type ConfigCompiled struct { - Timeout time.Duration - BaseURL urls.BaseURL - BaseURLLiveReload urls.BaseURL - KindOutputFormats map[string]output.Formats - DisabledKinds map[string]bool - DisabledLanguages map[string]bool - IgnoredErrors map[string]bool - CreateTitle func(s string) string - IsUglyURLSection func(section string) bool - IgnoreFile func(filename string) bool - MainSections []string - Clock time.Time + Timeout time.Duration + BaseURL urls.BaseURL + BaseURLLiveReload urls.BaseURL + ServerInterface string + KindOutputFormats map[string]output.Formats + DefaultOutputFormat output.Format + DisabledKinds map[string]bool + DisabledLanguages map[string]bool + IgnoredLogs map[string]bool + CreateTitle func(s string) string + IsUglyURLSection func(section string) bool + IgnoreFile func(filename string) bool + SegmentFilter segments.SegmentFilter + MainSections []string + Clock time.Time + HTTPCache httpcache.ConfigCompiled // This is set to the last transient error found during config compilation. // With themes/modules we compute the configuration in multiple passes, and @@ -370,24 +502,28 @@ type ConfigCompiled struct { } // This may be set after the config is compiled. -func (c *ConfigCompiled) SetMainSectionsIfNotSet(sections []string) { +func (c *ConfigCompiled) SetMainSections(sections []string) { c.mu.Lock() defer c.mu.Unlock() - if c.MainSections != nil { - return - } c.MainSections = sections } +// IsMainSectionsSet returns whether the main sections have been set. +func (c *ConfigCompiled) IsMainSectionsSet() bool { + c.mu.Lock() + defer c.mu.Unlock() + return c.MainSections != nil +} + // This is set after the config is compiled by the server command. -func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) { +func (c *ConfigCompiled) SetServerInfo(baseURL, baseURLLiveReload urls.BaseURL, serverInterface string) { c.BaseURL = baseURL c.BaseURLLiveReload = baseURLLiveReload + c.ServerInterface = serverInterface } // RootConfig holds all the top-level configuration options in Hugo type RootConfig struct { - // The base URL of the site. // Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly. // {"identifiers": ["URL"] } @@ -408,13 +544,20 @@ type RootConfig struct { // Copyright information. Copyright string - // The language to apply to content without any Clolanguage indicator. + // The language to apply to content without any language indicator. DefaultContentLanguage string - // By defefault, we put the default content language in the root and the others below their language ID, e.g. /no/. + // By default, we put the default content language in the root and the others below their language ID, e.g. /no/. // Set this to true to put all languages below their language ID. DefaultContentLanguageInSubdir bool + // The default output format to use for the site. + // If not set, we will use the first output format. + DefaultOutputFormat string + + // Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled. + DisableDefaultLanguageRedirect bool + // Disable creation of alias redirect pages. DisableAliases bool @@ -427,9 +570,16 @@ type RootConfig struct { // A list of languages to disable. DisableLanguages []string + // The named segments to render. + // This needs to match the name of the segment in the segments configuration. + RenderSegments []string + // Disable the injection of the Hugo generator tag on the home page. DisableHugoGeneratorInject bool + // Disable live reloading in server mode. + DisableLiveReload bool + // Enable replacement in Pages' Content of Emoji shortcodes with their equivalent Unicode characters. // {"identifiers": ["Content", "Unicode"] } EnableEmoji bool @@ -455,8 +605,8 @@ type RootConfig struct { // Enable to disable the build lock file. NoBuildLock bool - // A list of error IDs to ignore. - IgnoreErrors []string + // A list of log IDs to ignore. + IgnoreLogs []string // A list of regexps that match paths to ignore. // Deprecated: Use the settings on module imports. @@ -468,11 +618,8 @@ 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 // The configured environment. Default is "development" for server and "production" for build. Environment string @@ -484,15 +631,20 @@ type RootConfig struct { HasCJKLanguage bool // The default number of pages per page when paginating. + // Deprecated: Use the Pagination struct. Paginate int // The path to use when creating pagination URLs, e.g. "page" in /page/2/. + // Deprecated: Use the Pagination struct. PaginatePath string // Whether to pluralize default list titles. // Note that this currently only works for English, but you can provide your own title in the content file's front matter. PluralizeListTitles bool + // Whether to capitalize automatic page titles, applicable to section, taxonomy, and term pages. + CapitalizeListTitles bool + // Make all relative URLs absolute using the baseURL. // {"identifiers": ["baseURL"] } CanonifyURLs bool @@ -506,6 +658,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 @@ -526,7 +684,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. @@ -604,19 +762,30 @@ type Configs struct { LanguageConfigMap map[string]*Config LanguageConfigSlice []*Config - IsMultihost bool - Languages langs.Languages - LanguagesDefaultFirst langs.Languages + IsMultihost bool Modules modules.Modules ModulesClient *modules.Client + // All below is set in Init. + Languages langs.Languages + LanguagesDefaultFirst langs.Languages + ContentPathParser *paths.PathParser + configLangs []config.AllProvider } +func (c *Configs) Validate(logger loggers.Logger) error { + c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool { + page.CheckCascadePattern(logger, p) + return true + }) + return nil +} + // transientErr returns the last transient error found during config compilation. func (c *Configs) transientErr() error { - for _, l := range c.LanguageConfigSlice { + for _, l := range c.LanguageConfigMap { if l.C.transientErr != nil { return l.C.transientErr } @@ -630,6 +799,103 @@ func (c *Configs) IsZero() bool { } func (c *Configs) Init() error { + var languages langs.Languages + + var langKeys []string + var hasEn bool + + const en = "en" + + for k := range c.LanguageConfigMap { + langKeys = append(langKeys, k) + if k == en { + hasEn = true + } + } + + // Sort the LanguageConfigSlice by language weight (if set) or lang. + sort.Slice(langKeys, func(i, j int) bool { + ki := langKeys[i] + kj := langKeys[j] + lki := c.LanguageConfigMap[ki] + lkj := c.LanguageConfigMap[kj] + li := lki.Languages[ki] + lj := lkj.Languages[kj] + if li.Weight != lj.Weight { + return li.Weight < lj.Weight + } + return ki < kj + }) + + // See issue #13646. + defaultConfigLanguageFallback := en + if !hasEn { + // Pick the first one. + defaultConfigLanguageFallback = langKeys[0] + } + + if c.Base.DefaultContentLanguage == "" { + c.Base.DefaultContentLanguage = defaultConfigLanguageFallback + } + + for _, k := range langKeys { + v := c.LanguageConfigMap[k] + if v.DefaultContentLanguage == "" { + v.DefaultContentLanguage = defaultConfigLanguageFallback + } + c.LanguageConfigSlice = append(c.LanguageConfigSlice, v) + languageConf := v.Languages[k] + language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf) + if err != nil { + return err + } + languages = append(languages, language) + } + + // Filter out disabled languages. + var n int + for _, l := range languages { + if !l.Disabled { + languages[n] = l + n++ + } + } + languages = languages[:n] + + var languagesDefaultFirst langs.Languages + for _, l := range languages { + if l.Lang == c.Base.DefaultContentLanguage { + languagesDefaultFirst = append(languagesDefaultFirst, l) + } + } + for _, l := range languages { + if l.Lang != c.Base.DefaultContentLanguage { + languagesDefaultFirst = append(languagesDefaultFirst, l) + } + } + + c.Languages = languages + c.LanguagesDefaultFirst = languagesDefaultFirst + + c.ContentPathParser = &paths.PathParser{ + LanguageIndex: languagesDefaultFirst.AsIndexSet(), + IsLangDisabled: c.Base.IsLangDisabled, + IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix, + IsOutputFormat: func(name, ext string) bool { + if name == "" { + return false + } + + if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok { + if ext != "" && !of.MediaType.HasSuffix(ext) { + return false + } + return true + } + return false + }, + } + c.configLangs = make([]config.AllProvider, len(c.Languages)) for i, l := range c.LanguagesDefaultFirst { c.configLangs[i] = ConfigLanguage{ @@ -641,7 +907,7 @@ func (c *Configs) Init() error { } if len(c.Modules) == 0 { - return errors.New("no modules loaded (ned at least the main module)") + return errors.New("no modules loaded (need at least the main module)") } // Apply default project mounts. @@ -649,6 +915,26 @@ func (c *Configs) Init() error { return err } + // We should consolidate this, but to get a full view of the mounts in e.g. "hugo config" we need to + // transfer any default mounts added above to the config used to print the config. + for _, m := range c.Modules[0].Mounts() { + var found bool + for _, cm := range c.Base.Module.Mounts { + if cm.Source == m.Source && cm.Target == m.Target && cm.Lang == m.Lang { + found = true + break + } + } + if !found { + c.Base.Module.Mounts = append(c.Base.Module.Mounts, m) + } + } + + // Transfer the changed mounts to the language versions (all share the same mount set, but can be displayed in different languages). + for _, l := range c.LanguageConfigSlice { + l.Module.Mounts = c.Base.Module.Mounts + } + return nil } @@ -669,27 +955,59 @@ func (c Configs) GetByLang(lang string) config.AllProvider { return nil } +func newDefaultConfig() *Config { + return &Config{ + Taxonomies: map[string]string{"tag": "tags", "category": "categories"}, + Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, + RootConfig: RootConfig{ + Environment: hugo.EnvironmentProduction, + TitleCaseStyle: "AP", + PluralizeListTitles: true, + CapitalizeListTitles: true, + StaticDir: []string{"static"}, + SummaryLength: 70, + Timeout: "60s", + + CommonDirs: config.CommonDirs{ + ArcheTypeDir: "archetypes", + ContentDir: "content", + ResourceDir: "resources", + PublishDir: "public", + ThemesDir: "themes", + AssetDir: "assets", + LayoutDir: "layouts", + I18nDir: "i18n", + DataDir: "data", + }, + }, + } +} + // fromLoadConfigResult creates a new Config from res. func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) { if !res.Cfg.IsSet("languages") { // We need at least one lang := res.Cfg.GetString("defaultContentLanguage") + if lang == "" { + lang = "en" + } res.Cfg.Set("languages", maps.Params{lang: maps.Params{}}) } bcfg := res.BaseConfig cfg := res.Cfg - all := &Config{} - err := decodeConfigFromParams(fs, bcfg, cfg, all, nil) + all := newDefaultConfig() + + err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil) if err != nil { return nil, err } langConfigMap := make(map[string]*Config) - var langConfigs []*Config languagesConfig := cfg.GetStringMap("languages") - var isMultiHost bool + + var isMultihost bool if err := all.CompileConfig(logger); err != nil { return nil, err @@ -700,37 +1018,24 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon var differentRootKeys []string switch x := v.(type) { case maps.Params: - var params maps.Params - pv, found := x["params"] - if found { - params = pv.(maps.Params) - } else { - params = maps.Params{ + _, found := x["params"] + if !found { + x["params"] = maps.Params{ maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep, } - x["params"] = params } for kk, vv := range x { if kk == "_merge" { continue } - if kk != maps.MergeStrategyKey && !configLanguageKeys[kk] { - // This should have been placed below params. - // We accidentally allowed it in the past, so we need to support it a little longer, - // But log a warning. - if _, found := params[kk]; !found { - helpers.Deprecated(fmt.Sprintf("config: languages.%s.%s: custom params on the language top level", k, kk), fmt.Sprintf("Put the value below [languages.%s.params]. See https://gohugo.io/content-management/multilingual/#changes-in-hugo-01120", k), false) - params[kk] = vv - } - } if kk == "baseurl" { // baseURL configure don the language level is a multihost setup. - isMultiHost = true + isMultihost = true } mergedConfig.Set(kk, vv) - if cfg.IsSet(kk) { - rootv := cfg.Get(kk) + rootv := cfg.Get(kk) + if rootv != nil && cfg.IsSet(kk) { // This overrides a root key and potentially needs a merge. if !reflect.DeepEqual(rootv, vv) { switch vvv := vv.(type) { @@ -767,12 +1072,26 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon // Create a copy of the complete config and replace the root keys with the language specific ones. clone := all.cloneForLang() - if err := decodeConfigFromParams(fs, bcfg, mergedConfig, clone, differentRootKeys); err != nil { + + if err := decodeConfigFromParams(fs, logger, bcfg, mergedConfig, clone, differentRootKeys); err != nil { return nil, fmt.Errorf("failed to decode config for language %q: %w", k, err) } if err := clone.CompileConfig(logger); err != nil { return nil, err } + + // Adjust Goldmark config defaults for multilingual, single-host sites. + if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles { + if !clone.Markup.Goldmark.DuplicateResourceFiles { + if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil { + clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true) + } + if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil { + clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true) + } + } + } + langConfigMap[k] = clone case maps.ParamsMergeStrategy: default: @@ -781,61 +1100,24 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon } } - var languages langs.Languages - defaultContentLanguage := all.DefaultContentLanguage - for k, v := range langConfigMap { - languageConf := v.Languages[k] - language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf) - if err != nil { - return nil, err - } - languages = append(languages, language) - } - - // Sort the sites by language weight (if set) or lang. - sort.Slice(languages, func(i, j int) bool { - li := languages[i] - lj := languages[j] - if li.Weight != lj.Weight { - return li.Weight < lj.Weight - } - return li.Lang < lj.Lang - }) - - for _, l := range languages { - langConfigs = append(langConfigs, langConfigMap[l.Lang]) - } - - var languagesDefaultFirst langs.Languages - for _, l := range languages { - if l.Lang == defaultContentLanguage { - languagesDefaultFirst = append(languagesDefaultFirst, l) - } - } - for _, l := range languages { - if l.Lang != defaultContentLanguage { - languagesDefaultFirst = append(languagesDefaultFirst, l) - } - } - bcfg.PublishDir = all.PublishDir res.BaseConfig = bcfg + all.CommonDirs.CacheDir = bcfg.CacheDir + for _, l := range langConfigMap { + l.CommonDirs.CacheDir = bcfg.CacheDir + } cm := &Configs{ - Base: all, - LanguageConfigMap: langConfigMap, - LanguageConfigSlice: langConfigs, - LoadingInfo: res, - IsMultihost: isMultiHost, - Languages: languages, - LanguagesDefaultFirst: languagesDefaultFirst, + Base: all, + LanguageConfigMap: langConfigMap, + LoadingInfo: res, + IsMultihost: isMultihost, } return cm, nil } -func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error { - +func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error { var decoderSetups []decodeWeight if len(keys) == 0 { @@ -847,7 +1129,7 @@ func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provid if v, found := allDecoderSetups[key]; found { decoderSetups = append(decoderSetups, v) } else { - return fmt.Errorf("unknown config key %q", key) + logger.Warnf("Skip unknown config key %q", key) } } } @@ -884,11 +1166,11 @@ func createDefaultOutputFormats(allFormats output.Formats) map[string][]string { } m := map[string][]string{ - page.KindPage: {htmlOut.Name}, - page.KindHome: defaultListTypes, - page.KindSection: defaultListTypes, - page.KindTerm: defaultListTypes, - page.KindTaxonomy: defaultListTypes, + kinds.KindPage: {htmlOut.Name}, + kinds.KindHome: defaultListTypes, + kinds.KindSection: defaultListTypes, + kinds.KindTerm: defaultListTypes, + kinds.KindTaxonomy: defaultListTypes, } // May be disabled diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go new file mode 100644 index 000000000..8f6cacf84 --- /dev/null +++ b/config/allconfig/allconfig_integration_test.go @@ -0,0 +1,381 @@ +package allconfig_test + +import ( + "path/filepath" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/config/allconfig" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/media" +) + +func TestDirsMount(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ["taxonomy", "term"] +[languages] +[languages.en] +weight = 1 +[languages.sv] +weight = 2 +[[module.mounts]] +source = 'content/en' +target = 'content' +lang = 'en' +[[module.mounts]] +source = 'content/sv' +target = 'content' +lang = 'sv' +-- content/en/p1.md -- +--- +title: "p1" +--- +-- content/sv/p1.md -- +--- +title: "p1" +--- +-- layouts/_default/single.html -- +Title: {{ .Title }} + ` + + b := hugolib.NewIntegrationTestBuilder( + hugolib.IntegrationTestConfig{T: t, TxtarString: files}, + ).Build() + + // b.AssertFileContent("public/p1/index.html", "Title: p1") + + sites := b.H.Sites + b.Assert(len(sites), qt.Equals, 2) + + configs := b.H.Configs + mods := configs.Modules + b.Assert(len(mods), qt.Equals, 1) + mod := mods[0] + b.Assert(mod.Mounts(), qt.HasLen, 8) + + enConcp := sites[0].Conf + enConf := enConcp.GetConfig().(*allconfig.Config) + + b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com/") + modConf := enConf.Module + b.Assert(modConf.Mounts, qt.HasLen, 8) + b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en")) + b.Assert(modConf.Mounts[0].Target, qt.Equals, "content") + b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en") + b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv")) + b.Assert(modConf.Mounts[1].Target, qt.Equals, "content") + 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) +} + +func TestRedefineContentTypes(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[mediaTypes] +[mediaTypes."text/html"] +suffixes = ["html", "xhtml"] +` + + b := hugolib.Test(t, files) + + conf := b.H.Configs.Base + contentTypes := conf.ContentTypes.Config + + b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"}) + b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"}) +} + +func TestPaginationConfig(t *testing.T) { + files := ` +-- hugo.toml -- + [languages.en] + weight = 1 + [languages.en.pagination] + pagerSize = 20 + [languages.de] + weight = 2 + [languages.de.pagination] + path = "page-de" + +` + + b := hugolib.Test(t, files) + + confEn := b.H.Sites[0].Conf.Pagination() + confDe := b.H.Sites[1].Conf.Pagination() + + b.Assert(confEn.Path, qt.Equals, "page") + b.Assert(confEn.PagerSize, qt.Equals, 20) + b.Assert(confDe.Path, qt.Equals, "page-de") + b.Assert(confDe.PagerSize, qt.Equals, 10) +} + +func TestPaginationConfigDisableAliases(t *testing.T) { + files := ` +-- hugo.toml -- +disableKinds = ["taxonomy", "term"] +[pagination] +disableAliases = true +pagerSize = 2 +-- layouts/_default/list.html -- +{{ $paginator := .Paginate site.RegularPages }} +{{ template "_internal/pagination.html" . }} +{{ range $paginator.Pages }} + {{ .Title }} +{{ end }} +-- content/p1.md -- +--- +title: "p1" +--- +-- content/p2.md -- +--- +title: "p2" +--- +-- content/p3.md -- +--- +title: "p3" +--- +` + + b := hugolib.Test(t, files) + + b.AssertFileExists("public/page/1/index.html", false) + b.AssertFileContent("public/page/2/index.html", "pagination-default") +} + +func TestMapUglyURLs(t *testing.T) { + files := ` +-- hugo.toml -- +[uglyurls] + posts = true +` + + b := hugolib.Test(t, files) + + c := b.H.Configs.Base + + b.Assert(c.C.IsUglyURLSection("posts"), qt.IsTrue) + b.Assert(c.C.IsUglyURLSection("blog"), qt.IsFalse) +} + +// Issue 13199 +func TestInvalidOutputFormat(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +[outputs] +home = ['html','foo'] +-- layouts/index.html -- +x +` + + b, err := hugolib.TestE(t, files) + b.Assert(err, qt.IsNotNil) + b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`) +} + +// Issue 13201 +func TestLanguageConfigSlice(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +[languages.en] +title = 'TITLE_EN' +weight = 2 +[languages.de] +title = 'TITLE_DE' +weight = 1 +[languages.fr] +title = 'TITLE_FR' +weight = 3 +` + + b := hugolib.Test(t, files) + b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`) +} + +func TestContentTypesDefault(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" + + +` + + b := hugolib.Test(t, files) + + ct := b.H.Configs.Base.ContentTypes + c := ct.Config + s := ct.SourceStructure.(map[string]media.ContentTypeConfig) + + b.Assert(c.IsContentFile("foo.md"), qt.Equals, true) + b.Assert(len(s), qt.Equals, 6) +} + +func TestMergeDeep(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +theme = ["theme1", "theme2"] +_merge = "deep" +-- themes/theme1/hugo.toml -- +[sitemap] +filename = 'mysitemap.xml' +[services] +[services.googleAnalytics] +id = 'foo bar' +[taxonomies] + foo = 'bars' +-- themes/theme2/config/_default/hugo.toml -- +[taxonomies] + bar = 'baz' +-- layouts/home.html -- +GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}. + +` + + b := hugolib.Test(t, files) + + conf := b.H.Configs + base := conf.Base + + b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction) + b.Assert(base.BaseURL, qt.Equals, "https://example.com") + b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml") + b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"}) + + b.AssertFileContent("public/index.html", "GA ID: foo bar.") +} + +func TestMergeDeepBuildStats(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +title = "Theme 1" +_merge = "deep" +[module] +[module.hugoVersion] +[[module.imports]] +path = "theme1" +-- themes/theme1/hugo.toml -- +[build] +[build.buildStats] +disableIDs = true +enable = true +-- layouts/home.html -- +Home. + +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + + conf := b.H.Configs + base := conf.Base + + b.Assert(base.Title, qt.Equals, "Theme 1") + b.Assert(len(base.Module.Imports), qt.Equals, 1) + b.Assert(base.Build.BuildStats.Enable, qt.Equals, true) + b.AssertFileExists("/hugo_stats.json", true) +} + +func TestMergeDeepBuildStatsTheme(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +_merge = "deep" +theme = ["theme1"] +-- themes/theme1/hugo.toml -- +title = "Theme 1" +[build] +[build.buildStats] +disableIDs = true +enable = true +-- layouts/home.html -- +Home. + +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + + conf := b.H.Configs + base := conf.Base + + b.Assert(base.Title, qt.Equals, "Theme 1") + b.Assert(len(base.Module.Imports), qt.Equals, 1) + b.Assert(base.Build.BuildStats.Enable, qt.Equals, true) + b.AssertFileExists("/hugo_stats.json", true) +} + +func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[languages] +[languages.nn] +weight = 20 +[languages.sv] +weight = 10 +[languages.sv.taxonomies] + tag = "taggar" +-- layouts/all.html -- +All. +` + + b := hugolib.Test(t, files) + + b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv") +} + +func TestDefaultConfigEnvDisableLanguagesIssue13707(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +disableLanguages = [] +[languages] +[languages.en] +weight = 1 +[languages.nn] +weight = 2 +[languages.sv] +weight = 3 +` + + b := hugolib.Test(t, files, hugolib.TestOptWithConfig(func(conf *hugolib.IntegrationTestConfig) { + conf.Environ = []string{`HUGO_DISABLELANGUAGES=sv nn`} + })) + + b.Assert(len(b.H.Sites), qt.Equals, 1) +} diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index c8944bd2d..035349790 100644 --- a/config/allconfig/alldecoders.go +++ b/config/allconfig/alldecoders.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -18,18 +18,22 @@ import ( "strings" "github.com/gohugoio/hugo/cache/filecache" + + "github.com/gohugoio/hugo/cache/httpcache" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/services" - "github.com/gohugoio/hugo/deploy" + "github.com/gohugoio/hugo/deploy/deployconfig" + "github.com/gohugoio/hugo/hugolib/segments" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/minifiers" "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/related" @@ -49,10 +53,11 @@ type decodeConfig struct { } type decodeWeight struct { - key string - decode func(decodeWeight, decodeConfig) error - getCompiler func(c *Config) configCompiler - weight int + key string + decode func(decodeWeight, decodeConfig) error + getCompiler func(c *Config) configCompiler + weight int + internalOrDeprecated bool // Hide it from the docs. } var allDecoderSetups = map[string]decodeWeight{ @@ -60,7 +65,14 @@ var allDecoderSetups = map[string]decodeWeight{ key: "", weight: -100, // Always first. decode: func(d decodeWeight, p decodeConfig) error { - return mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig) + if err := mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig); err != nil { + return err + } + + // This need to match with Lang which is always lower case. + p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage) + + return nil }, }, "imaging": { @@ -86,6 +98,18 @@ var allDecoderSetups = map[string]decodeWeight{ return err }, }, + "httpcache": { + key: "httpcache", + decode: func(d decodeWeight, p decodeConfig) error { + var err error + p.c.HTTPCache, err = httpcache.DecodeConfig(p.bcfg, p.p.GetStringMap(d.key)) + if p.c.IgnoreCache { + p.c.HTTPCache.Cache.For.Excludes = []string{"**"} + p.c.HTTPCache.Cache.For.Includes = []string{} + } + return err + }, + }, "build": { key: "build", decode: func(d decodeWeight, p decodeConfig) error { @@ -112,6 +136,14 @@ var allDecoderSetups = map[string]decodeWeight{ return err }, }, + "segments": { + key: "segments", + decode: func(d decodeWeight, p decodeConfig) error { + var err error + p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key)) + return err + }, + }, "server": { key: "server", decode: func(d decodeWeight, p decodeConfig) error { @@ -131,8 +163,17 @@ var allDecoderSetups = map[string]decodeWeight{ return err }, }, - "mediaTypes": { - key: "mediaTypes", + "contenttypes": { + key: "contenttypes", + weight: 100, // This needs to be decoded after media types. + decode: func(d decodeWeight, p decodeConfig) error { + var err error + p.c.ContentTypes, err = media.DecodeContentTypes(p.p.GetStringMap(d.key), p.c.MediaTypes.Config) + return err + }, + }, + "mediatypes": { + key: "mediatypes", decode: func(d decodeWeight, p decodeConfig) error { var err error p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key)) @@ -143,7 +184,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) @@ -161,8 +202,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)) @@ -199,22 +240,27 @@ 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": { key: "sitemap", decode: func(d decodeWeight, p decodeConfig) error { var err error - p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key)) + if p.p.IsSet(d.key) { + p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key)) + } return err }, }, "taxonomies": { key: "taxonomies", decode: func(d decodeWeight, p decodeConfig) error { - p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + if p.p.IsSet(d.key) { + p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + } return nil }, }, @@ -231,7 +277,7 @@ var allDecoderSetups = map[string]decodeWeight{ } else { p.c.Related = related.DefaultConfig if _, found := p.c.Taxonomies["tag"]; found { - p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80}) + p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80, Type: related.TypeBasic}) } } return nil @@ -245,12 +291,17 @@ var allDecoderSetups = map[string]decodeWeight{ if len(m) == 1 { // In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level. var first maps.Params + var ok bool for _, v := range m { - first = v.(maps.Params) - break + first, ok = v.(maps.Params) + if ok { + break + } } - if _, found := first["languagecode"]; !found { - first["languagecode"] = p.p.GetString("languagecode") + if first != nil { + if _, found := first["languagecode"]; !found { + first["languagecode"] = p.p.GetString("languagecode") + } } } p.c.Languages, err = langs.DecodeConfig(m) @@ -258,6 +309,20 @@ var allDecoderSetups = map[string]decodeWeight{ return err } + // Validate defaultContentLanguage. + if p.c.DefaultContentLanguage != "" { + var found bool + for lang := range p.c.Languages { + if lang == p.c.DefaultContentLanguage { + found = true + break + } + } + if !found { + return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage) + } + } + return nil }, }, @@ -265,7 +330,7 @@ var allDecoderSetups = map[string]decodeWeight{ key: "cascade", decode: func(d decodeWeight, p decodeConfig) error { var err error - p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key)) + p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key)) return err }, }, @@ -277,6 +342,41 @@ var allDecoderSetups = map[string]decodeWeight{ return err }, }, + "page": { + key: "page", + decode: func(d decodeWeight, p decodeConfig) error { + p.c.Page = config.PageConfig{ + NextPrevSortOrder: "desc", + NextPrevInSectionSortOrder: "desc", + } + if p.p.IsSet(d.key) { + if err := mapstructure.WeakDecode(p.p.Get(d.key), &p.c.Page); err != nil { + return err + } + } + + return nil + }, + getCompiler: func(c *Config) configCompiler { + return &c.Page + }, + }, + "pagination": { + key: "pagination", + decode: func(d decodeWeight, p decodeConfig) error { + p.c.Pagination = config.Pagination{ + PagerSize: 10, + Path: "page", + } + if p.p.IsSet(d.key) { + if err := mapstructure.WeakDecode(p.p.Get(d.key), &p.c.Pagination); err != nil { + return err + } + } + + return nil + }, + }, "privacy": { key: "privacy", decode: func(d decodeWeight, p decodeConfig) error { @@ -305,23 +405,25 @@ var allDecoderSetups = map[string]decodeWeight{ key: "deployment", decode: func(d decodeWeight, p decodeConfig) error { var err error - p.c.Deployment, err = deploy.DecodeConfig(p.p) + p.c.Deployment, err = deployconfig.DecodeConfig(p.p) return err }, }, "author": { key: "author", decode: func(d decodeWeight, p decodeConfig) error { - p.c.Author = p.p.GetStringMap(d.key) + p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key)) return nil }, + internalOrDeprecated: true, }, "social": { key: "social", decode: func(d decodeWeight, p decodeConfig) error { - p.c.Social = p.p.GetStringMapString(d.key) + p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) return nil }, + internalOrDeprecated: true, }, "uglyurls": { key: "uglyurls", @@ -332,16 +434,36 @@ var allDecoderSetups = map[string]decodeWeight{ p.c.UglyURLs = vv case string: p.c.UglyURLs = vv == "true" + case maps.Params: + p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv)) default: p.c.UglyURLs = cast.ToStringMapBool(v) } return nil }, + internalOrDeprecated: true, }, "internal": { key: "internal", decode: func(d decodeWeight, p decodeConfig) error { return mapstructure.WeakDecode(p.p.GetStringMap(d.key), &p.c.Internal) }, + internalOrDeprecated: true, }, } + +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)) + } + } +} diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index 95c5c7edf..6990a3590 100644 --- a/config/allconfig/configlanguage.go +++ b/config/allconfig/configlanguage.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -16,8 +16,10 @@ package allconfig import ( "time" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/langs" ) @@ -41,6 +43,21 @@ func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages { return c.m.LanguagesDefaultFirst } +func (c ConfigLanguage) PathParser() *paths.PathParser { + return c.m.ContentPathParser +} + +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 } @@ -54,10 +71,17 @@ func (c ConfigLanguage) Environment() string { } func (c ConfigLanguage) IsMultihost() bool { + if len(c.m.Languages)-len(c.config.C.DisabledLanguages) <= 1 { + return false + } return c.m.IsMultihost } -func (c ConfigLanguage) IsMultiLingual() bool { +func (c ConfigLanguage) FastRenderMode() bool { + return c.config.Internal.FastRenderMode +} + +func (c ConfigLanguage) IsMultilingual() bool { return len(c.m.Languages) > 1 } @@ -73,8 +97,8 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool { return c.config.C.DisabledLanguages[lang] } -func (c ConfigLanguage) IgnoredErrors() map[string]bool { - return c.config.C.IgnoredErrors +func (c ConfigLanguage) IgnoredLogs() map[string]bool { + return c.config.C.IgnoredLogs } func (c ConfigLanguage) NoBuildLock() bool { @@ -109,6 +133,21 @@ func (c ConfigLanguage) Quiet() bool { return c.m.Base.Internal.Quiet } +func (c ConfigLanguage) Watching() bool { + return c.m.Base.Internal.Watch +} + +func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager { + if !c.Watching() { + return identity.NopManager + } + return identity.NewManager(name, opts...) +} + +func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider { + return c.config.ContentTypes.Config +} + // GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use. func (c ConfigLanguage) GetConfigSection(s string) any { switch s { @@ -130,10 +169,12 @@ func (c ConfigLanguage) GetConfigSection(s string) any { return c.config.Permalinks case "minify": return c.config.Minify - case "activeModules": + case "allModules": return c.m.Modules case "deployment": return c.config.Deployment + case "httpCacheCompiled": + return c.config.C.HTTPCache default: panic("not implemented: " + s) } @@ -199,22 +240,22 @@ 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 { return c.config.C.CreateTitle(s) } -func (c ConfigLanguage) Paginate() int { - return c.config.Paginate -} - -func (c ConfigLanguage) PaginatePath() string { - return c.config.PaginatePath +func (c ConfigLanguage) Pagination() config.Pagination { + return c.config.Pagination } func (c ConfigLanguage) StaticDirs() []string { return c.config.staticDirs() } + +func (c ConfigLanguage) EnableEmoji() bool { + return c.config.EnableEmoji +} diff --git a/config/allconfig/docshelper.go b/config/allconfig/docshelper.go new file mode 100644 index 000000000..1a5fb6153 --- /dev/null +++ b/config/allconfig/docshelper.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 allconfig + +import ( + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/docshelper" +) + +// This is is just some helpers used to create some JSON used in the Hugo docs. +func init() { + docsProvider := func() docshelper.DocProvider { + cfg := config.New() + for configRoot, v := range allDecoderSetups { + if v.internalOrDeprecated { + continue + } + cfg.Set(configRoot, make(maps.Params)) + } + lang := maps.Params{ + "en": maps.Params{ + "menus": maps.Params{}, + "params": maps.Params{}, + }, + } + cfg.Set("languages", lang) + cfg.SetDefaultMergeStrategy() + + configHelpers := map[string]any{ + "mergeStrategy": cfg.Get(""), + } + return docshelper.DocProvider{"config_helpers": configHelpers} + } + + docshelper.AddDocProviderFunc(docsProvider) +} diff --git a/config/allconfig/integration_test.go b/config/allconfig/integration_test.go deleted file mode 100644 index e96dbd296..000000000 --- a/config/allconfig/integration_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package allconfig_test - -import ( - "path/filepath" - "testing" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config/allconfig" - "github.com/gohugoio/hugo/hugolib" -) - -func TestDirsMount(t *testing.T) { - - files := ` --- hugo.toml -- -baseURL = "https://example.com" -disableKinds = ["taxonomy", "term"] -[languages] -[languages.en] -weight = 1 -[languages.sv] -weight = 2 -[[module.mounts]] -source = 'content/en' -target = 'content' -lang = 'en' -[[module.mounts]] -source = 'content/sv' -target = 'content' -lang = 'sv' --- content/en/p1.md -- ---- -title: "p1" ---- --- content/sv/p1.md -- ---- -title: "p1" ---- --- layouts/_default/single.html -- -Title: {{ .Title }} - ` - - b := hugolib.NewIntegrationTestBuilder( - hugolib.IntegrationTestConfig{T: t, TxtarString: files}, - ).Build() - - //b.AssertFileContent("public/p1/index.html", "Title: p1") - - sites := b.H.Sites - b.Assert(len(sites), qt.Equals, 2) - - configs := b.H.Configs - mods := configs.Modules - b.Assert(len(mods), qt.Equals, 1) - mod := mods[0] - b.Assert(mod.Mounts(), qt.HasLen, 8) - - enConcp := sites[0].Conf - enConf := enConcp.GetConfig().(*allconfig.Config) - - b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com") - modConf := enConf.Module - b.Assert(modConf.Mounts, qt.HasLen, 2) - b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en")) - b.Assert(modConf.Mounts[0].Target, qt.Equals, "content") - b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en") - b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv")) - b.Assert(modConf.Mounts[1].Target, qt.Equals, "content") - b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv") - -} diff --git a/config/allconfig/load.go b/config/allconfig/load.go index ad090d60d..4fb8bbaef 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -37,6 +37,7 @@ import ( "github.com/spf13/afero" ) +//lint:ignore ST1005 end user message. var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n") func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { @@ -45,7 +46,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { } if d.Logger == nil { - d.Logger = loggers.NewErrorLogger() + d.Logger = loggers.NewDefault() } l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()} @@ -63,7 +64,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to create config from result: %w", err) } - moduleConfig, modulesClient, err := l.loadModules(configs) + moduleConfig, modulesClient, err := l.loadModules(configs, d.IgnoreModuleDoesNotExist) if err != nil { return nil, fmt.Errorf("failed to load modules: %w", err) } @@ -78,19 +79,21 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { if err := configs.transientErr(); err != nil { return nil, fmt.Errorf("failed to create config from modules config: %w", err) } + configs.LoadingInfo.ConfigFiles = append(configs.LoadingInfo.ConfigFiles, l.ModulesConfigFiles...) } else if err := configs.transientErr(); err != nil { return nil, fmt.Errorf("failed to create config: %w", err) } - configs.Modules = moduleConfig.ActiveModules + configs.Modules = moduleConfig.AllModules configs.ModulesClient = modulesClient if err := configs.Init(); err != nil { return nil, fmt.Errorf("failed to init config: %w", err) } - return configs, nil + loggers.SetGlobalLogger(d.Logger) + return configs, nil } // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). @@ -113,6 +116,9 @@ type ConfigSourceDescriptor struct { // Defaults to os.Environ if not set. Environ []string + + // If set, this will be used to ignore the module does not exist error. + IgnoreModuleDoesNotExist bool } func (d ConfigSourceDescriptor) configFilenames() []string { @@ -134,7 +140,12 @@ 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"}, + {Key: "ignoreErrors", Value: "ignoreLogs"}, + } for _, alias := range aliases { if l.cfg.IsSet(alias.Key) { @@ -148,62 +159,9 @@ func (l configLoader) applyConfigAliases() error { func (l configLoader) applyDefaultConfig() error { defaultSettings := maps.Params{ - "baseURL": "", - "cleanDestinationDir": false, - "watch": false, - "contentDir": "content", - "resourceDir": "resources", - "publishDir": "public", - "publishDirOrig": "public", - "themesDir": "themes", - "assetDir": "assets", - "layoutDir": "layouts", - "i18nDir": "i18n", - "dataDir": "data", - "archetypeDir": "archetypes", - "configDir": "config", - "staticDir": "static", - "buildDrafts": false, - "buildFuture": false, - "buildExpired": false, - "params": maps.Params{}, - "environment": hugo.EnvironmentProduction, - "uglyURLs": false, - "verbose": false, - "ignoreCache": false, - "canonifyURLs": false, - "relativeURLs": false, - "removePathAccents": false, - "titleCaseStyle": "AP", - "taxonomies": maps.Params{"tag": "tags", "category": "categories"}, - "permalinks": maps.Params{}, - "sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"}, - "menus": maps.Params{}, - "disableLiveReload": false, - "pluralizeListTitles": true, - "forceSyncStatic": false, - "footnoteAnchorPrefix": "", - "footnoteReturnLinkContents": "", - "newContentEditor": "", - "paginate": 10, - "paginatePath": "page", - "summaryLength": 70, - "rssLimit": -1, - "sectionPagesMenu": "", - "disablePathToLower": false, - "hasCJKLanguage": false, - "enableEmoji": false, - "defaultContentLanguage": "en", - "defaultContentLanguageInSubdir": false, - "enableMissingTranslationPlaceholders": false, - "enableGitInfo": false, - "ignoreFiles": make([]string, 0), - "disableAliases": false, - "debug": false, - "disableFastRender": false, - "timeout": "30s", - "timeZone": "", - "enableInlineShortcodes": false, + // These dirs are used early/before we build the config struct. + "themesDir": "themes", + "configDir": "config", } l.cfg.SetDefaults(defaultSettings) @@ -275,26 +233,64 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error { if existing != nil { val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing) - if err != nil { + if err == nil { + val = l.envValToVal(env.Key, val) + if owner != nil { + owner[nestedKey] = val + } else { + l.cfg.Set(env.Key, val) + } continue } + } - if owner != nil { - owner[nestedKey] = val - } else { - l.cfg.Set(env.Key, val) - } - } else if nestedKey != "" { + if owner != nil && nestedKey != "" { owner[nestedKey] = env.Value } else { - // The container does not exist yet. - l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value) + var val any + key := strings.ReplaceAll(env.Key, delim, ".") + _, ok := allDecoderSetups[key] + if ok { + // A map. + if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]any{}); err == nil { + val = v + } + } + + if val == nil { + // A string. + val = l.envStringToVal(key, env.Value) + } + l.cfg.Set(key, val) } + } return nil } +func (l *configLoader) envValToVal(k string, v any) any { + switch v := v.(type) { + case string: + return l.envStringToVal(k, v) + default: + return v + } +} + +func (l *configLoader) envStringToVal(k, v string) any { + switch k { + case "disablekinds", "disablelanguages": + if strings.Contains(v, ",") { + return strings.Split(v, ",") + } else { + return strings.Fields(v) + } + default: + return v + } +} + func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) { var res config.LoadConfigResult @@ -417,11 +413,12 @@ func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConf return res, l.ModulesConfig, err } -func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) { +func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bool) (modules.ModulesConfig, *modules.Client, error) { bcfg := configs.LoadingInfo.BaseConfig conf := configs.Base workingDir := bcfg.WorkingDir themesDir := bcfg.ThemesDir + publishDir := bcfg.PublishDir cfg := configs.LoadingInfo.Cfg @@ -430,10 +427,10 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) } - ex := hexec.New(conf.Security) + ex := hexec.New(conf.Security, workingDir, l.Logger) hook := func(m *modules.ModulesConfig) error { - for _, tc := range m.ActiveModules { + for _, tc := range m.AllModules { if len(tc.ConfigFilenames()) > 0 { if tc.Watch() { l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...) @@ -450,16 +447,18 @@ func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *mo } modulesClient := modules.NewClient(modules.ClientConfig{ - Fs: l.Fs, - Logger: l.Logger, - Exec: ex, - HookBeforeFinalize: hook, - WorkingDir: workingDir, - ThemesDir: themesDir, - Environment: l.Environment, - CacheDir: conf.Caches.CacheDirModules(), - ModuleConfig: conf.Module, - IgnoreVendor: ignoreVendor, + Fs: l.Fs, + Logger: l.Logger, + Exec: ex, + HookBeforeFinalize: hook, + WorkingDir: workingDir, + ThemesDir: themesDir, + PublishDir: publishDir, + Environment: l.Environment, + CacheDir: conf.Caches.CacheDirModules(), + ModuleConfig: conf.Module, + IgnoreVendor: ignoreVendor, + IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist, }) moduleConfig, err := modulesClient.Collect() @@ -533,15 +532,6 @@ func (l configLoader) deleteMergeStrategies() { }) } -func (l configLoader) loadModulesConfig() (modules.Config, error) { - modConfig, err := modules.DecodeConfig(l.cfg) - if err != nil { - return modules.Config{}, err - } - - return modConfig, nil -} - func (l configLoader) wrapFileError(err error, filename string) error { fe := herrors.UnwrapFileError(err) if fe != nil { diff --git a/config/allconfig/load_test.go b/config/allconfig/load_test.go index 153a59c44..3c16e71e9 100644 --- a/config/allconfig/load_test.go +++ b/config/allconfig/load_test.go @@ -50,7 +50,7 @@ weight = 3 title = "Svenska" weight = 4 ` - if err := os.WriteFile(configFilename, []byte(config), 0666); err != nil { + if err := os.WriteFile(configFilename, []byte(config), 0o666); err != nil { b.Fatal(err) } d := ConfigSourceDescriptor{ diff --git a/config/commonConfig.go b/config/commonConfig.go index bd3e235bd..947078672 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -15,10 +15,13 @@ package config import ( "fmt" + "net/http" "regexp" + "slices" "sort" "strings" + "github.com/bep/logg" "github.com/gobwas/glob" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/types" @@ -81,38 +84,28 @@ type LoadConfigResult struct { var defaultBuild = BuildConfig{ UseResourceCacheWhen: "fallback", - WriteStats: false, + BuildStats: BuildStats{}, CacheBusters: []CacheBuster{ - { - Source: `assets/.*\.(js|ts|jsx|tsx)`, - Target: `(js|scripts|javascript)`, - }, - { - Source: `assets/.*\.(css|sass|scss)$`, - Target: cssTargetCachebusterRe, - }, { Source: `(postcss|tailwind)\.config\.js`, Target: cssTargetCachebusterRe, }, - // This is deliberatly coarse grained; it will cache bust resources with "json" in the cache key when js files changes, which is good. - { - Source: `assets/.*\.(.*)$`, - Target: `$1`, - }, }, } // BuildConfig holds some build related configuration. type BuildConfig struct { - UseResourceCacheWhen string // never, fallback, always. Default is fallback + // When to use the resource file cache. + // One of never, fallback, always. Default is fallback + UseResourceCacheWhen string // 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 intellinsense /assets/jsconfig.js + // Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js // file. NoJSConfigInAssets bool @@ -120,8 +113,23 @@ 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...) + b.CacheBusters = slices.Clone(b.CacheBusters) return b } @@ -131,7 +139,7 @@ func (b BuildConfig) UseResourceCache(err error) bool { } if b.UseResourceCacheWhen == "fallback" { - return err == herrors.ErrFeatureNotAvailable + return herrors.IsFeatureNotAvailableError(err) } return true @@ -170,14 +178,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) @@ -197,6 +213,8 @@ type SitemapConfig struct { Priority float64 // The sitemap filename. Filename string + // Whether to disable page inclusion. + Disable bool } func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) { @@ -210,7 +228,22 @@ type Server struct { Redirects []Redirect compiledHeaders []glob.Glob - compiledRedirects []glob.Glob + compiledRedirects []redirect +} + +type redirect struct { + from glob.Glob + fromRe *regexp.Regexp + headers map[string]glob.Glob +} + +func (r redirect) matchHeader(header http.Header) bool { + for k, v := range r.headers { + if !v.Match(header.Get(k)) { + return false + } + } + return true } func (s *Server) CompileConfig(logger loggers.Logger) error { @@ -218,10 +251,41 @@ func (s *Server) CompileConfig(logger loggers.Logger) error { return nil } for _, h := range s.Headers { - s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For)) + g, err := glob.Compile(h.For) + if err != nil { + return fmt.Errorf("failed to compile Headers glob %q: %w", h.For, err) + } + s.compiledHeaders = append(s.compiledHeaders, g) } for _, r := range s.Redirects { - s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From)) + if r.From == "" && r.FromRe == "" { + return fmt.Errorf("redirects must have either From or FromRe set") + } + rd := redirect{ + headers: make(map[string]glob.Glob), + } + if r.From != "" { + g, err := glob.Compile(r.From) + if err != nil { + return fmt.Errorf("failed to compile Redirect glob %q: %w", r.From, err) + } + rd.from = g + } + if r.FromRe != "" { + re, err := regexp.Compile(r.FromRe) + if err != nil { + return fmt.Errorf("failed to compile Redirect regexp %q: %w", r.FromRe, err) + } + rd.fromRe = re + } + for k, v := range r.FromHeaders { + g, err := glob.Compile(v) + if err != nil { + return fmt.Errorf("failed to compile Redirect header glob %q: %w", v, err) + } + rd.headers[k] = g + } + s.compiledRedirects = append(s.compiledRedirects, rd) } return nil @@ -250,22 +314,42 @@ func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr { return matches } -func (s *Server) MatchRedirect(pattern string) Redirect { +func (s *Server) MatchRedirect(pattern string, header http.Header) Redirect { if s.compiledRedirects == nil { return Redirect{} } pattern = strings.TrimSuffix(pattern, "index.html") - for i, g := range s.compiledRedirects { + for i, r := range s.compiledRedirects { redir := s.Redirects[i] - // No redirect to self. - if redir.To == pattern { - return Redirect{} + var found bool + + if r.from != nil { + if r.from.Match(pattern) { + found = header == nil || r.matchHeader(header) + // We need to do regexp group replacements if needed. + } } - if g.Match(pattern) { + if r.fromRe != nil { + m := r.fromRe.FindStringSubmatch(pattern) + if m != nil { + if !found { + found = header == nil || r.matchHeader(header) + } + + if found { + // Replace $1, $2 etc. in To. + for i, g := range m[1:] { + redir.To = strings.ReplaceAll(redir.To, fmt.Sprintf("$%d", i+1), g) + } + } + } + } + + if found { return redir } } @@ -279,8 +363,22 @@ type Headers struct { } type Redirect struct { + // From is the Glob pattern to match. + // One of From or FromRe must be set. From string - To string + + // FromRe is the regexp to match. + // This regexp can contain group matches (e.g. $1) that can be used in the To field. + // One of From or FromRe must be set. + FromRe string + + // To is the target URL. + To string + + // Headers to match for the redirect. + // This maps the HTTP header name to a Glob pattern with values to match. + // If the map is empty, the redirect will always be triggered. + FromHeaders map[string]string // HTTP status code to use for the redirect. // A status code of 200 will trigger a URL rewrite. @@ -306,13 +404,16 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if c.compiledSource != nil { return nil } + source := c.Source - target := c.Target sourceRe, err := regexp.Compile(source) if err != nil { return fmt.Errorf("failed to compile cache buster source %q: %w", c.Source, err) } + target := c.Target var compileErr error + debugl := logger.Logger().WithLevel(logg.LevelDebug).WithField(loggers.FieldNameCmd, "cachebuster") + c.compiledSource = func(s string) func(string) bool { m := sourceRe.FindStringSubmatch(s) matchString := "no match" @@ -320,38 +421,37 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error { if match { matchString = "match!" } - logger.Debugf("cachebuster: Matching %q with source %q: %s\n", s, source, matchString) + debugl.Logf("Matching %q with source %q: %s", s, source, matchString) if !match { return nil } groups := m[1:] + currentTarget := target // Replace $1, $2 etc. in target. - for i, g := range groups { - target = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g) + currentTarget = strings.ReplaceAll(target, fmt.Sprintf("$%d", i+1), g) } - targetRe, err := regexp.Compile(target) + targetRe, err := regexp.Compile(currentTarget) if err != nil { - compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", target, err) + compileErr = fmt.Errorf("failed to compile cache buster target %q: %w", currentTarget, err) return nil } - return func(s string) bool { - match = targetRe.MatchString(s) + return func(ss string) bool { + match = targetRe.MatchString(ss) matchString := "no match" if match { matchString = "match!" } - logger.Debugf("cachebuster: Matching %q with target %q: %s\n", s, target, matchString) + logger.Debugf("Matching %q with target %q: %s", ss, currentTarget, matchString) return match } - } return compileErr } func (r Redirect) IsZero() bool { - return r.From == "" + return r.From == "" && r.FromRe == "" } const ( @@ -365,17 +465,7 @@ func DecodeServer(cfg Provider) (Server, error) { _ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s) for i, redir := range s.Redirects { - // Get it in line with the Hugo server for OK responses. - // We currently treat the 404 as a special case, they are always "ugly", so keep them as is. - if redir.Status != 404 { - redir.To = strings.TrimSuffix(redir.To, "index.html") - if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") { - // There are some tricky infinite loop situations when dealing - // when the target does not have a trailing slash. - // This can certainly be handled better, but not time for that now. - return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To) - } - } + redir.To = strings.TrimSuffix(redir.To, "index.html") s.Redirects[i] = redir } @@ -383,13 +473,39 @@ func DecodeServer(cfg Provider) (Server, error) { // Set up a default redirect for 404s. s.Redirects = []Redirect{ { - From: "**", + From: "/**", To: "/404.html", Status: 404, }, } - } return *s, nil } + +// Pagination configures the pagination behavior. +type Pagination struct { + // Default number of elements per pager in pagination. + PagerSize int + + // The path element used during pagination. + Path string + + // Whether to disable generation of alias for the first pagination page. + DisableAliases bool +} + +// PageConfig configures the behavior of pages. +type PageConfig struct { + // Sort order for Page.Next and Page.Prev. Default "desc" (the default page sort order in Hugo). + NextPrevSortOrder string + + // Sort order for Page.NextInSection and Page.PrevInSection. Default "desc". + NextPrevInSectionSortOrder string +} + +func (c *PageConfig) CompileConfig(loggers.Logger) error { + c.NextPrevInSectionSortOrder = strings.ToLower(c.NextPrevInSectionSortOrder) + c.NextPrevSortOrder = strings.ToLower(c.NextPrevSortOrder) + return nil +} diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index 106069bdc..05ba185e3 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -71,7 +71,28 @@ X-Content-Type-Options = "nosniff" [[server.redirects]] from = "/foo/**" -to = "/foo/index.html" +to = "/baz/index.html" +status = 200 + +[[server.redirects]] +from = "/loop/**" +to = "/loop/foo/" +status = 200 + +[[server.redirects]] +from = "/b/**" +fromRe = "/b/(.*)/" +to = "/baz/$1/" +status = 200 + +[[server.redirects]] +fromRe = "/c/(.*)/" +to = "/boo/$1/" +status = 200 + +[[server.redirects]] +fromRe = "/d/(.*)/" +to = "/boo/$1/" status = 200 [[server.redirects]] @@ -79,11 +100,6 @@ from = "/google/**" to = "https://google.com/" status = 301 -[[server.redirects]] -from = "/**" -to = "/default/index.html" -status = 301 - `, "toml") @@ -92,7 +108,7 @@ status = 301 s, err := DecodeServer(cfg) c.Assert(err, qt.IsNil) - c.Assert(s.CompileConfig(loggers.NewErrorLogger()), qt.IsNil) + c.Assert(s.CompileConfig(loggers.NewDefault()), qt.IsNil) c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{ {Key: "X-Content-Type-Options", Value: "nosniff"}, @@ -100,67 +116,82 @@ status = 301 {Key: "X-XSS-Protection", Value: "1; mode=block"}, }) - c.Assert(s.MatchRedirect("/foo/bar/baz"), qt.DeepEquals, Redirect{ + c.Assert(s.MatchRedirect("/foo/bar/baz", nil), qt.DeepEquals, Redirect{ From: "/foo/**", - To: "/foo/", + To: "/baz/", Status: 200, }) - c.Assert(s.MatchRedirect("/someother"), qt.DeepEquals, Redirect{ - From: "/**", - To: "/default/", - Status: 301, + c.Assert(s.MatchRedirect("/foo/bar/", nil), qt.DeepEquals, Redirect{ + From: "/foo/**", + To: "/baz/", + Status: 200, }) - c.Assert(s.MatchRedirect("/google/foo"), qt.DeepEquals, Redirect{ + c.Assert(s.MatchRedirect("/b/c/", nil), qt.DeepEquals, Redirect{ + From: "/b/**", + FromRe: "/b/(.*)/", + To: "/baz/c/", + Status: 200, + }) + + c.Assert(s.MatchRedirect("/c/d/", nil).To, qt.Equals, "/boo/d/") + c.Assert(s.MatchRedirect("/c/d/e/", nil).To, qt.Equals, "/boo/d/e/") + + c.Assert(s.MatchRedirect("/someother", nil), qt.DeepEquals, Redirect{}) + + c.Assert(s.MatchRedirect("/google/foo", nil), qt.DeepEquals, Redirect{ From: "/google/**", To: "https://google.com/", Status: 301, }) - - // No redirect loop, please. - c.Assert(s.MatchRedirect("/default/index.html"), qt.DeepEquals, Redirect{}) - c.Assert(s.MatchRedirect("/default/"), qt.DeepEquals, Redirect{}) - - for _, errorCase := range []string{ - `[[server.redirects]] -from = "/**" -to = "/file" -status = 301`, - `[[server.redirects]] -from = "/**" -to = "/foo/file.html" -status = 301`, - } { - - cfg, err := FromConfigString(errorCase, "toml") - c.Assert(err, qt.IsNil) - _, err = DecodeServer(cfg) - c.Assert(err, qt.Not(qt.IsNil)) - - } } func TestBuildConfigCacheBusters(t *testing.T) { c := qt.New(t) cfg := New() conf := DecodeBuildConfig(cfg) - l := loggers.NewInfoLogger() + l := loggers.NewDefault() c.Assert(conf.CompileConfig(l), qt.IsNil) - m, err := conf.MatchCacheBuster(l, "assets/foo/main.js") - c.Assert(err, qt.IsNil) + m, _ := conf.MatchCacheBuster(l, "tailwind.config.js") c.Assert(m, qt.IsNotNil) - c.Assert(m("scripts"), qt.IsTrue) - c.Assert(m("asdf"), qt.IsFalse) - - m, _ = conf.MatchCacheBuster(l, "tailwind.config.js") c.Assert(m("css"), qt.IsTrue) c.Assert(m("js"), qt.IsFalse) - m, err = conf.MatchCacheBuster(l, "assets/foo.json") - c.Assert(err, qt.IsNil) - c.Assert(m, qt.IsNotNil) - c.Assert(m("json"), qt.IsTrue) - + m, _ = conf.MatchCacheBuster(l, "foo.bar") + c.Assert(m, qt.IsNil) +} + +func TestBuildConfigCacheBusterstTailwindSetup(t *testing.T) { + c := qt.New(t) + cfg := New() + cfg.Set("build", map[string]any{ + "cacheBusters": []map[string]string{ + { + "source": "assets/watching/hugo_stats\\.json", + "target": "css", + }, + { + "source": "(postcss|tailwind)\\.config\\.js", + "target": "css", + }, + { + "source": "assets/.*\\.(js|ts|jsx|tsx)", + "target": "js", + }, + { + "source": "assets/.*\\.(.*)$", + "target": "$1", + }, + }, + }) + + conf := DecodeBuildConfig(cfg) + l := loggers.NewDefault() + c.Assert(conf.CompileConfig(l), qt.IsNil) + + m, err := conf.MatchCacheBuster(l, "assets/watching/hugo_stats.json") + c.Assert(err, qt.IsNil) + c.Assert(m("css"), qt.IsTrue) } diff --git a/config/configLoader.go b/config/configLoader.go index 6e520b9cc..dd103f27b 100644 --- a/config/configLoader.go +++ b/config/configLoader.go @@ -157,7 +157,7 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid if err != nil { // This will be used in error reporting, use the most specific value. dirnames = []string{path} - return fmt.Errorf("failed to unmarshl config for path %q: %w", path, err) + return fmt.Errorf("failed to unmarshal config for path %q: %w", path, err) } var keyPath []string @@ -208,7 +208,6 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid } return cfg, dirnames, nil - } var keyAliases maps.KeyRenamer diff --git a/config/configProvider.go b/config/configProvider.go index 8ed0728bd..c21342dce 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -17,8 +17,10 @@ import ( "time" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/urls" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/langs" ) @@ -27,16 +29,19 @@ type AllProvider interface { Language() *langs.Language Languages() langs.Languages LanguagesDefaultFirst() langs.Languages + LanguagePrefix() string BaseURL() urls.BaseURL BaseURLLiveReload() urls.BaseURL + PathParser() *paths.PathParser Environment() string IsMultihost() bool - IsMultiLingual() bool + IsMultilingual() bool NoBuildLock() bool BaseConfig() BaseConfig Dirs() CommonDirs Quiet() bool DirsBase() CommonDirs + ContentTypes() ContentTypesProvider GetConfigSection(string) any GetConfig() any CanonifyURLs() bool @@ -47,24 +52,36 @@ type AllProvider interface { DefaultContentLanguageInSubdir() bool IsLangDisabled(string) bool SummaryLength() int - Paginate() int - PaginatePath() string + Pagination() Pagination BuildExpired() bool BuildFuture() bool BuildDrafts() bool Running() bool + Watching() bool + NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager + FastRenderMode() bool PrintUnusedTemplates() bool EnableMissingTranslationPlaceholders() bool TemplateMetrics() bool TemplateMetricsHints() bool - LogI18nWarnings() bool + PrintI18nWarnings() bool CreateTitle(s string) string IgnoreFile(s string) bool NewContentEditor() string Timeout() time.Duration StaticDirs() []string - IgnoredErrors() map[string]bool + IgnoredLogs() map[string]bool WorkingDir() string + EnableEmoji() bool +} + +// We cannot import the media package as that would create a circular dependency. +// This interface defines a subset of what media.ContentTypes provides. +type ContentTypesProvider interface { + IsContentSuffix(suffix string) bool + IsContentFile(filename string) bool + IsIndexContentFile(filename string) bool + IsHTMLSuffix(suffix string) bool } // Provider provides the configuration settings for Hugo. @@ -93,9 +110,3 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string { sd := cfg.Get(key) return types.ToStringSlicePreserveString(sd) } - -func setIfNotSet(cfg Provider, key string, value any) { - if !cfg.IsSet(key) { - cfg.Set(key, value) - } -} diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go index e8a08e281..8c1d63851 100644 --- a/config/defaultConfigProvider.go +++ b/config/defaultConfigProvider.go @@ -15,7 +15,6 @@ package config import ( "fmt" - "sort" "strings" "sync" @@ -26,42 +25,6 @@ import ( "github.com/gohugoio/hugo/common/maps" ) -var ( - - // ConfigRootKeysSet contains all of the config map root keys. - ConfigRootKeysSet = map[string]bool{ - "build": true, - "caches": true, - "cascade": true, - "frontmatter": true, - "languages": true, - "imaging": true, - "markup": true, - "mediatypes": true, - "menus": true, - "minify": true, - "module": true, - "outputformats": true, - "params": true, - "permalinks": true, - "related": true, - "sitemap": true, - "privacy": true, - "security": true, - "taxonomies": true, - } - - // ConfigRootKeys is a sorted version of ConfigRootKeysSet. - ConfigRootKeys []string -) - -func init() { - for k := range ConfigRootKeysSet { - ConfigRootKeys = append(ConfigRootKeys, k) - } - sort.Strings(ConfigRootKeys) -} - // New creates a Provider backed by an empty maps.Params. func New() Provider { return &defaultConfigProvider{ @@ -370,7 +333,6 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() { } return false }) - } func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) { @@ -383,7 +345,7 @@ func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (str c.keyCache.Store(key, parts) } current := c.root - for i := 0; i < len(parts)-1; i++ { + for i := range len(parts) - 1 { next, found := current[parts[i]] if !found { if create { diff --git a/config/defaultConfigProvider_test.go b/config/defaultConfigProvider_test.go index 65f10ec6a..cd6247e60 100644 --- a/config/defaultConfigProvider_test.go +++ b/config/defaultConfigProvider_test.go @@ -332,7 +332,7 @@ func TestDefaultConfigProvider(t *testing.T) { return nil } - for i := 0; i < 20; i++ { + for i := range 20 { i := i r.Run(func() error { const v = 42 diff --git a/config/docshelper.go b/config/docshelper.go deleted file mode 100644 index e34c53c2b..000000000 --- a/config/docshelper.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2021 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 config - -import ( - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/docshelper" -) - -// This is is just some helpers used to create some JSON used in the Hugo docs. -func init() { - docsProvider := func() docshelper.DocProvider { - - cfg := New() - for _, configRoot := range ConfigRootKeys { - cfg.Set(configRoot, make(maps.Params)) - } - lang := maps.Params{ - "en": maps.Params{ - "menus": maps.Params{}, - "params": maps.Params{}, - }, - } - cfg.Set("languages", lang) - cfg.SetDefaultMergeStrategy() - - configHelpers := map[string]any{ - "mergeStrategy": cfg.Get(""), - } - return docshelper.DocProvider{"config": configHelpers} - } - - docshelper.AddDocProviderFunc(docsProvider) -} diff --git a/config/env.go b/config/env.go index 1e9266b17..4dcd63653 100644 --- a/config/env.go +++ b/config/env.go @@ -18,6 +18,12 @@ import ( "runtime" "strconv" "strings" + + "github.com/pbnjay/memory" +) + +const ( + gigabyte = 1 << 30 ) // GetNumWorkerMultiplier returns the base value used to calculate the number @@ -33,6 +39,36 @@ func GetNumWorkerMultiplier() int { return runtime.NumCPU() } +// GetMemoryLimit returns the upper memory limit in bytes for Hugo's in-memory caches. +// Note that this does not represent "all of the memory" that Hugo will use, +// so it needs to be set to a lower number than the available system memory. +// It will read from the HUGO_MEMORYLIMIT (in Gigabytes) environment variable. +// If that is not set, it will set aside a quarter of the total system memory. +func GetMemoryLimit() uint64 { + if mem := os.Getenv("HUGO_MEMORYLIMIT"); mem != "" { + if v := stringToGibabyte(mem); v > 0 { + return v + } + } + + // There is a FreeMemory function, but as the kernel in most situations + // will take whatever memory that is left and use for caching etc., + // that value is not something that we can use. + m := memory.TotalMemory() + if m != 0 { + return uint64(m / 4) + } + + return 2 * gigabyte +} + +func stringToGibabyte(f string) uint64 { + if v, err := strconv.ParseFloat(f, 32); err == nil && v > 0 { + return uint64(v * gigabyte) + } + return 0 +} + // SetEnvVars sets vars on the form key=value in the oldVars slice. func SetEnvVars(oldVars *[]string, keyValues ...string) { for i := 0; i < len(keyValues); i += 2 { diff --git a/config/namespace.go b/config/namespace.go index 3ecd01014..e41b56e2d 100644 --- a/config/namespace.go +++ b/config/namespace.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -16,14 +16,13 @@ package config import ( "encoding/json" - "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/common/hashing" ) func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) { - // Calculate the hash of the input (not including any defaults applied later). // This allows us to introduce new config options without breaking the hash. - h := identity.HashString(configSource) + h := hashing.HashStringHex(configSource) // Build the config c, ext, err := buildConfig(configSource) diff --git a/config/namespace_test.go b/config/namespace_test.go index 008237c13..f443523a4 100644 --- a/config/namespace_test.go +++ b/config/namespace_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -26,10 +26,10 @@ func TestNamespace(t *testing.T) { c := qt.New(t) c.Assert(true, qt.Equals, true) - //ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig) + // ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig) ns, err := DecodeNamespace[[]*tstNsExt]( - map[string]interface{}{"foo": "bar"}, + map[string]any{"foo": "bar"}, func(v any) (*tstNsExt, any, error) { t := &tstNsExt{} m, err := maps.ToStringMapE(v) @@ -42,27 +42,19 @@ func TestNamespace(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(ns, qt.Not(qt.IsNil)) - c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"}) - c.Assert(ns.SourceHash, qt.Equals, "14368731254619220105") + c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]any{"foo": "bar"}) + c.Assert(ns.SourceHash, qt.Equals, "1420f6c7782f7459") c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"}) c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil)) - } type ( tstNsExt struct { Foo string } - tstNsInt struct { - Foo string - } ) func (t *tstNsExt) Init() error { t.Foo = strings.ToUpper(t.Foo) return nil } -func (t *tstNsInt) Compile(ext *tstNsExt) error { - t.Foo = ext.Foo + " qux" - return nil -} diff --git a/config/privacy/privacyConfig.go b/config/privacy/privacyConfig.go index a36046364..900f73540 100644 --- a/config/privacy/privacyConfig.go +++ b/config/privacy/privacyConfig.go @@ -30,9 +30,10 @@ type Config struct { Disqus Disqus GoogleAnalytics GoogleAnalytics Instagram Instagram - Twitter Twitter + Twitter Twitter // deprecated in favor of X in v0.141.0 Vimeo Vimeo YouTube YouTube + X X } // Disqus holds the privacy configuration settings related to the Disqus template. @@ -44,15 +45,9 @@ type Disqus struct { type GoogleAnalytics struct { Service `mapstructure:",squash"` - // Enabling this will disable the use of Cookies and use Session Storage to Store the GA Client ID. - UseSessionStorage bool - // Enabling this will make the GA templates respect the // "Do Not Track" HTTP header. See https://www.paulfurley.com/google-analytics-dnt/. RespectDoNotTrack bool - - // Enabling this will make it so the users' IP addresses are anonymized within Google Analytics. - AnonymizeIP bool } // Instagram holds the privacy configuration settings related to the Instagram shortcode. @@ -64,7 +59,8 @@ type Instagram struct { Simple bool } -// Twitter holds the privacy configuration settingsrelated to the Twitter shortcode. +// Twitter holds the privacy configuration settings related to the Twitter shortcode. +// Deprecated in favor of X in v0.141.0. type Twitter struct { Service `mapstructure:",squash"` @@ -76,7 +72,7 @@ type Twitter struct { Simple bool } -// Vimeo holds the privacy configuration settingsrelated to the Vimeo shortcode. +// Vimeo holds the privacy configuration settings related to the Vimeo shortcode. type Vimeo struct { Service `mapstructure:",squash"` @@ -90,7 +86,7 @@ type Vimeo struct { Simple bool } -// YouTube holds the privacy configuration settingsrelated to the YouTube shortcode. +// YouTube holds the privacy configuration settings related to the YouTube shortcode. type YouTube struct { Service `mapstructure:",squash"` @@ -100,6 +96,20 @@ type YouTube struct { PrivacyEnhanced bool } +// X holds the privacy configuration settings related to the X shortcode. +type X struct { + Service `mapstructure:",squash"` + + // When set to true, the X post and its embedded page on your site are not + // used for purposes that include personalized suggestions and personalized + // ads. + EnableDNT bool + + // If simple mode is enabled, a static and no-JS version of the X post will + // be built. + Simple bool +} + // DecodeConfig creates a privacy Config from a given Hugo configuration. func DecodeConfig(cfg config.Provider) (pc Config, err error) { if !cfg.IsSet(privacyConfigKey) { diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go index c17ce713d..1dd20215b 100644 --- a/config/privacy/privacyConfig_test.go +++ b/config/privacy/privacyConfig_test.go @@ -33,12 +33,10 @@ disable = true [privacy.googleAnalytics] disable = true respectDoNotTrack = true -anonymizeIP = true -useSessionStorage = true [privacy.instagram] disable = true simple = true -[privacy.twitter] +[privacy.x] disable = true enableDNT = true simple = true @@ -60,11 +58,11 @@ simple = true got := []bool{ pc.Disqus.Disable, pc.GoogleAnalytics.Disable, - pc.GoogleAnalytics.RespectDoNotTrack, pc.GoogleAnalytics.AnonymizeIP, - pc.GoogleAnalytics.UseSessionStorage, pc.Instagram.Disable, - pc.Instagram.Simple, pc.Twitter.Disable, pc.Twitter.EnableDNT, - pc.Twitter.Simple, pc.Vimeo.Disable, pc.Vimeo.EnableDNT, pc.Vimeo.Simple, - pc.YouTube.PrivacyEnhanced, pc.YouTube.Disable, + pc.GoogleAnalytics.RespectDoNotTrack, pc.Instagram.Disable, + pc.Instagram.Simple, + pc.Vimeo.Disable, pc.Vimeo.EnableDNT, pc.Vimeo.Simple, + pc.YouTube.PrivacyEnhanced, pc.YouTube.Disable, pc.X.Disable, pc.X.EnableDNT, + pc.X.Simple, } c.Assert(got, qt.All(qt.Equals), true) diff --git a/config/security/docshelper.go b/config/security/docshelper.go deleted file mode 100644 index ade03560e..000000000 --- a/config/security/docshelper.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2021 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 security - -import ( - "github.com/gohugoio/hugo/docshelper" -) - -func init() { - docsProvider := func() docshelper.DocProvider { - - return docshelper.DocProvider{"config": DefaultConfig.ToSecurityMap()} - } - docshelper.AddDocProviderFunc(docsProvider) -} diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go index f7d2beac8..a3ec5197d 100644 --- a/config/security/securityConfig.go +++ b/config/security/securityConfig.go @@ -34,22 +34,24 @@ const securityConfigKey = "security" // DefaultConfig holds the default security policy. var DefaultConfig = Config{ Exec: Exec{ - Allow: NewWhitelist( - "^dart-sass-embedded$", - "^go$", // for Go Modules - "^npx$", // used by all Node tools (Babel, PostCSS). + Allow: MustNewWhitelist( + "^(dart-)?sass(-embedded)?$", // sass, dart-sass, dart-sass-embedded. + "^go$", // for Go Modules + "^git$", // For Git info + "^npx$", // used by all Node tools (Babel, PostCSS). "^postcss$", + "^tailwindcss$", ), // 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|SYSTEMDRIVE)$`), }, 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"), }, } @@ -115,7 +117,6 @@ func (c Config) CheckAllowedExec(name string) error { } } return nil - } func (c Config) CheckAllowedGetEnv(name string) error { @@ -164,7 +165,6 @@ func (c Config) ToSecurityMap() map[string]any { "security": m, } return sec - } // DecodeConfig creates a privacy Config from a given Hugo configuration. @@ -194,23 +194,21 @@ func DecodeConfig(cfg config.Provider) (Config, error) { } return sc, nil - } func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType { return func( f reflect.Type, t reflect.Type, - data any) (any, error) { - + data any, + ) (any, error) { if t != reflect.TypeOf(Whitelist{}) { return data, nil } wl := types.ToStringSlicePreserveString(data) - return NewWhitelist(wl...), nil - + return NewWhitelist(wl...) } } diff --git a/config/security/securityConfig_test.go b/config/security/securityConfig_test.go index edc1737e3..faa05a97f 100644 --- a/config/security/securityConfig_test.go +++ b/config/security/securityConfig_test.go @@ -53,7 +53,6 @@ getEnv=["a", "b"] c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) c.Assert(pc.Funcs.Getenv.Accept("a"), qt.IsTrue) c.Assert(pc.Funcs.Getenv.Accept("c"), qt.IsFalse) - }) c.Run("String whitelist", func(c *qt.C) { @@ -80,7 +79,6 @@ osEnv="b" c.Assert(pc.Exec.Allow.Accept("d"), qt.IsFalse) c.Assert(pc.Exec.OsEnv.Accept("b"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) - }) c.Run("Default exec.osEnv", func(c *qt.C) { @@ -105,7 +103,6 @@ allow="a" c.Assert(pc.Exec.Allow.Accept("a"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("PATH"), qt.IsTrue) c.Assert(pc.Exec.OsEnv.Accept("e"), qt.IsFalse) - }) c.Run("Enable inline shortcodes, legacy", func(c *qt.C) { @@ -129,9 +126,7 @@ osEnv="b" pc, err := DecodeConfig(cfg) c.Assert(err, qt.IsNil) c.Assert(pc.EnableInlineShortcodes, qt.IsTrue) - }) - } func TestToTOML(t *testing.T) { @@ -140,7 +135,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.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", + "[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^git$', '^npx$', '^postcss$', '^tailwindcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']", ) } @@ -154,9 +149,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 +159,9 @@ 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) - } diff --git a/config/security/whitelist.go b/config/security/whitelist.go index 72a80da2e..5ce369a1f 100644 --- a/config/security/whitelist.go +++ b/config/security/whitelist.go @@ -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,20 +68,33 @@ func NewWhitelist(patterns ...string) Whitelist { if !acceptSome { return Whitelist{ acceptNone: true, - } + }, nil } var patternsr []*regexp.Regexp - for i := 0; i < len(patterns); i++ { + for i := range patterns { p := strings.TrimSpace(patterns[i]) 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. diff --git a/config/security/whitelist_test.go b/config/security/whitelist_test.go index 5c4196dff..add3345a8 100644 --- a/config/security/whitelist_test.go +++ b/config/security/whitelist_test.go @@ -24,24 +24,23 @@ 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) }) - } diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go index 1b4317e92..f9d5e1a6e 100644 --- a/config/services/servicesConfig.go +++ b/config/services/servicesConfig.go @@ -31,7 +31,8 @@ type Config struct { Disqus Disqus GoogleAnalytics GoogleAnalytics Instagram Instagram - Twitter Twitter + Twitter Twitter // deprecated in favor of X in v0.141.0 + X X RSS RSS } @@ -61,6 +62,7 @@ type Instagram struct { } // Twitter holds the functional configuration settings related to the Twitter shortcodes. +// Deprecated in favor of X in v0.141.0. type Twitter struct { // The Simple variant of Twitter is decorated with a basic set of inline styles. // This means that if you want to provide your own CSS, you want @@ -68,6 +70,14 @@ type Twitter struct { DisableInlineCSS bool } +// X holds the functional configuration settings related to the X shortcodes. +type X struct { + // The Simple variant of X is decorated with a basic set of inline styles. + // This means that if you want to provide your own CSS, you want + // to disable the inline CSS provided by Hugo. + DisableInlineCSS bool +} + // RSS holds the functional configuration settings related to the RSS feeds. type RSS struct { // Limit the number of pages. @@ -91,6 +101,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) { if c.RSS.Limit == 0 { c.RSS.Limit = cfg.GetInt(rssLimitKey) + if c.RSS.Limit == 0 { + c.RSS.Limit = -1 + } } return diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go index 12b042a5a..952a7fe1c 100644 --- a/config/services/servicesConfig_test.go +++ b/config/services/servicesConfig_test.go @@ -36,6 +36,8 @@ id = "ga_id" disableInlineCSS = true [services.twitter] disableInlineCSS = true +[services.x] +disableInlineCSS = true ` cfg, err := config.FromConfigString(tomlConfig, "toml") c.Assert(err, qt.IsNil) diff --git a/config/testconfig/testconfig.go b/config/testconfig/testconfig.go index 4b47d82d1..8f70e6cb7 100644 --- a/config/testconfig/testconfig.go +++ b/config/testconfig/testconfig.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Hugo Authors. All rights reserved. +// Copyright 2024 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. @@ -36,17 +36,16 @@ func GetTestConfigs(fs afero.Fs, cfg config.Provider) *allconfig.Configs { // Make sure that the workingDir exists. workingDir := cfg.GetString("workingDir") if workingDir != "" { - if err := fs.MkdirAll(workingDir, 0777); err != nil { + if err := fs.MkdirAll(workingDir, 0o777); err != nil { panic(err) } } - configs, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg}) + configs, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg, Environ: []string{"EMPTY_TEST_ENVIRONMENT"}}) if err != nil { panic(err) } return configs - } func GetTestConfig(fs afero.Fs, cfg config.Provider) config.AllProvider { diff --git a/create/content.go b/create/content.go index 55159c24c..a4661c1ba 100644 --- a/create/content.go +++ b/create/content.go @@ -16,6 +16,7 @@ package create import ( "bytes" + "errors" "fmt" "io" "os" @@ -25,12 +26,9 @@ import ( "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/common/hstrings" "github.com/gohugoio/hugo/common/paths" - "errors" - - "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/helpers" @@ -42,7 +40,7 @@ const ( // DefaultArchetypeTemplateTemplate is the template used in 'hugo new site' // and the template we use as a fall back. DefaultArchetypeTemplateTemplate = `--- -title: "{{ replace .Name "-" " " | title }}" +title: "{{ replace .File.ContentBaseName "-" " " | title }}" date: {{ .Date }} draft: true --- @@ -53,7 +51,7 @@ draft: true // NewContent creates a new content file in h (or a full bundle if the archetype is a directory) // in targetPath. func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error { - if h.BaseFs.Content.Dirs == nil { + if _, err := h.BaseFs.Content.Fs.Stat(""); err != nil { return errors.New("no existing content directory configured for this project") } @@ -84,11 +82,13 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error b.setArcheTypeFilenameToUse(ext) withBuildLock := func() (string, error) { - unlock, err := h.BaseFs.LockBuild() - if err != nil { - return "", fmt.Errorf("failed to acquire a build lock: %s", err) + if !h.Configs.Base.NoBuildLock { + unlock, err := h.BaseFs.LockBuild() + if err != nil { + return "", fmt.Errorf("failed to acquire a build lock: %s", err) + } + defer unlock() } - defer unlock() if b.isDir { return "", b.buildDir() @@ -98,12 +98,11 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath) } - if !files.IsContentFile(b.targetPath) { + if !h.Conf.ContentTypes().IsContentFile(b.targetPath) { return "", fmt.Errorf("target path %q is not a known content format", b.targetPath) } return b.buildFile() - } filename, err := withBuildLock() @@ -116,7 +115,6 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error } return nil - } type contentBuilder struct { @@ -128,12 +126,12 @@ type contentBuilder struct { cf hugolib.ContentFactory // Builder state - archetypeFilename string - targetPath string - kind string - isDir bool - dirMap archetypeMap - force bool + archetypeFi hugofs.FileMetaInfo + targetPath string + kind string + isDir bool + dirMap archetypeMap + force bool } func (b *contentBuilder) buildDir() error { @@ -146,7 +144,10 @@ func (b *contentBuilder) buildDir() error { var baseDir string for _, fi := range b.dirMap.contentFiles { - targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename)) + + targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().PathInfo.Path(), b.archetypeFi.Meta().PathInfo.Path())) + + // ===> post/my-post/pages/bio.md abs, err := b.cf.CreateContentPlaceHolder(targetFilename, b.force) if err != nil { return err @@ -170,7 +171,6 @@ func (b *contentBuilder) buildDir() error { } return false }) - } if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil { @@ -178,22 +178,20 @@ func (b *contentBuilder) buildDir() error { } for i, filename := range contentTargetFilenames { - if err := b.applyArcheType(filename, b.dirMap.contentFiles[i].Meta().Path); err != nil { + if err := b.applyArcheType(filename, b.dirMap.contentFiles[i]); err != nil { return err } } // Copy the rest as is. - for _, f := range b.dirMap.otherFiles { - meta := f.Meta() - filename := meta.Path + for _, fi := range b.dirMap.otherFiles { + meta := fi.Meta() in, err := meta.Open() if err != nil { return fmt.Errorf("failed to open non-content file: %w", err) } - - targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(filename, b.archetypeFilename)) + targetFilename := filepath.Join(baseDir, b.targetPath, strings.TrimPrefix(fi.Meta().Filename, b.archetypeFi.Meta().Filename)) targetDir := filepath.Dir(targetFilename) if err := b.sourceFs.MkdirAll(targetDir, 0o777); err != nil && !os.IsExist(err) { @@ -225,7 +223,7 @@ func (b *contentBuilder) buildFile() (string, error) { return "", err } - usesSite, err := b.usesSiteVar(b.archetypeFilename) + usesSite, err := b.usesSiteVar(b.archetypeFi) if err != nil { return "", err } @@ -243,7 +241,7 @@ func (b *contentBuilder) buildFile() (string, error) { return "", err } - if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFilename); err != nil { + if err := b.applyArcheType(contentPlaceholderAbsFilename, b.archetypeFi); err != nil { return "", err } @@ -264,15 +262,14 @@ func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) { for _, p := range pathsToCheck { fi, err := b.archeTypeFs.Stat(p) if err == nil { - b.archetypeFilename = p + b.archetypeFi = fi.(hugofs.FileMetaInfo) b.isDir = fi.IsDir() return } } - } -func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename string) error { +func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugofs.FileMetaInfo) error { p := b.h.GetContentPage(contentFilename) if p == nil { panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename)) @@ -284,32 +281,39 @@ func (b *contentBuilder) applyArcheType(contentFilename, archetypeFilename strin } defer f.Close() - if archetypeFilename == "" { + if archetypeFi == nil { return b.cf.ApplyArchetypeTemplate(f, p, b.kind, DefaultArchetypeTemplateTemplate) } - return b.cf.ApplyArchetypeFilename(f, p, b.kind, archetypeFilename) - + return b.cf.ApplyArchetypeFi(f, p, b.kind, archetypeFi) } func (b *contentBuilder) mapArcheTypeDir() error { var m archetypeMap - walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil { - return err - } + seen := map[hstrings.Strings2]bool{} - if fi.IsDir() { + walkFn := func(path string, fim hugofs.FileMetaInfo) error { + if fim.IsDir() { return nil } - fil := fi.(hugofs.FileMetaInfo) + pi := fim.Meta().PathInfo - if files.IsContentFile(path) { - m.contentFiles = append(m.contentFiles, fil) + if pi.IsContent() { + pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang} + if seen[pathLang] { + // Duplicate content file, e.g. page.md and page.html. + // In the regular build, we will filter out the duplicates, but + // for archetype folders these are ambiguous and we need to + // fail. + return fmt.Errorf("duplicate content file found in archetype folder: %q; having both e.g. %s.md and %s.html is ambigous", path, pi.BaseNameNoIdentifier(), pi.BaseNameNoIdentifier()) + } + seen[pathLang] = true + m.contentFiles = append(m.contentFiles, fim) if !m.siteUsed { - m.siteUsed, err = b.usesSiteVar(path) + var err error + m.siteUsed, err = b.usesSiteVar(fim) if err != nil { return err } @@ -317,7 +321,7 @@ func (b *contentBuilder) mapArcheTypeDir() error { return nil } - m.otherFiles = append(m.otherFiles, fil) + m.otherFiles = append(m.otherFiles, fim) return nil } @@ -325,13 +329,13 @@ func (b *contentBuilder) mapArcheTypeDir() error { walkCfg := hugofs.WalkwayConfig{ WalkFn: walkFn, Fs: b.archeTypeFs, - Root: b.archetypeFilename, + Root: filepath.FromSlash(b.archetypeFi.Meta().PathInfo.Path()), } w := hugofs.NewWalkway(walkCfg) if err := w.Walk(); err != nil { - return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFilename, err) + return fmt.Errorf("failed to walk archetype dir %q: %w", b.archetypeFi.Meta().Filename, err) } b.dirMap = m @@ -370,17 +374,21 @@ func (b *contentBuilder) openInEditorIfConfigured(filename string) error { return cmd.Run() } -func (b *contentBuilder) usesSiteVar(filename string) (bool, error) { - if filename == "" { +func (b *contentBuilder) usesSiteVar(fi hugofs.FileMetaInfo) (bool, error) { + if fi == nil { return false, nil } - bb, err := afero.ReadFile(b.archeTypeFs, filename) + f, err := fi.Meta().Open() if err != nil { - return false, fmt.Errorf("failed to open archetype file: %w", err) + return false, err + } + defer f.Close() + bb, err := io.ReadAll(f) + if err != nil { + return false, fmt.Errorf("failed to read archetype file: %w", err) } return bytes.Contains(bb, []byte(".Site")) || bytes.Contains(bb, []byte("site.")), nil - } type archetypeMap struct { diff --git a/create/content_test.go b/create/content_test.go index 77c6ca6c9..429edfc26 100644 --- a/create/content_test.go +++ b/create/content_test.go @@ -114,58 +114,6 @@ func TestNewContentFromFile(t *testing.T) { } } -func TestNewContentFromDir(t *testing.T) { - mm := afero.NewMemMapFs() - c := qt.New(t) - - archetypeDir := filepath.Join("archetypes", "my-bundle") - c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil) - - archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle") - c.Assert(mm.MkdirAll(archetypeThemeDir, 0o755), qt.IsNil) - - contentFile := ` -File: %s -Site Lang: {{ .Site.Language.Lang }} -Name: {{ replace .Name "-" " " | title }} -i18n: {{ T "hugo" }} -` - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0o755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - - c.Assert(initFs(mm), qt.IsNil) - cfg, fs := newTestCfg(c, mm) - - conf := testconfig.GetTestConfigs(fs.Source, cfg) - h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs}) - c.Assert(err, qt.IsNil) - c.Assert(len(h.Sites), qt.Equals, 2) - - c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`) - - // Content files should get the correct site context. - // TODO(bep) archetype check i18n - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Site Lang: nn`, `Name: My Post`, `i18n: Hugo Rokkar!`) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: Bio`) - - c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post", false), qt.IsNil) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) -} - func TestNewContentFromDirSiteFunction(t *testing.T) { mm := afero.NewMemMapFs() c := qt.New(t) @@ -181,7 +129,7 @@ site RegularPages: {{ len site.RegularPages }} ` - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil) + c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), fmt.Appendf(nil, contentFile, "index.md"), 0o755), qt.IsNil) c.Assert(afero.WriteFile(mm, filepath.Join(defaultArchetypeDir, "index.md"), []byte("default archetype index.md"), 0o755), qt.IsNil) c.Assert(initFs(mm), qt.IsNil) @@ -206,83 +154,6 @@ site RegularPages: {{ len site.RegularPages }} // Regular files should fall back to the default archetype (we have no regular file archetype). c.Assert(create.NewContent(h, "my-bundle", "mypage.md", false), qt.IsNil) cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "mypage.md")), `draft: true`) - -} - -func TestNewContentFromDirNoSite(t *testing.T) { - mm := afero.NewMemMapFs() - c := qt.New(t) - - archetypeDir := filepath.Join("archetypes", "my-bundle") - c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil) - - archetypeThemeDir := filepath.Join("themes", "mytheme", "archetypes", "my-theme-bundle") - c.Assert(mm.MkdirAll(archetypeThemeDir, 0o755), qt.IsNil) - - contentFile := ` -File: %s -Name: {{ replace .Name "-" " " | title }} -i18n: {{ T "hugo" }} -` - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(fmt.Sprintf(contentFile, "index.nn.md")), 0o755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "pages", "bio.md"), []byte(fmt.Sprintf(contentFile, "bio.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "resources", "hugo2.xml"), []byte(`hugo2: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeThemeDir, "resources", "hugo1.json"), []byte(`hugo1: {{ printf "no template handling in here" }}`), 0o755), qt.IsNil) - - c.Assert(initFs(mm), qt.IsNil) - cfg, fs := newTestCfg(c, mm) - conf := testconfig.GetTestConfigs(fs.Source, cfg) - h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs}) - c.Assert(err, qt.IsNil) - c.Assert(len(h.Sites), qt.Equals, 2) - - c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `File: index.md`, `Name: My Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.nn.md")), `File: index.nn.md`, `Name: My Post`, `i18n: Hugo Rokkar!`) - - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Name: Bio`) - - c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post", false), qt.IsNil) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Name: My Theme Post`, `i18n: Hugo Rocks!`) - cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`) -} - -func TestNewContentForce(t *testing.T) { - mm := afero.NewMemMapFs() - c := qt.New(t) - - archetypeDir := filepath.Join("archetypes", "my-bundle") - c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(""), 0o755), qt.IsNil) - c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(""), 0o755), qt.IsNil) - - c.Assert(initFs(mm), qt.IsNil) - cfg, fs := newTestCfg(c, mm) - - conf := testconfig.GetTestConfigs(fs.Source, cfg) - h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs}) - c.Assert(err, qt.IsNil) - c.Assert(len(h.Sites), qt.Equals, 2) - - // from file - c.Assert(create.NewContent(h, "post", "post/my-post.md", false), qt.IsNil) - c.Assert(create.NewContent(h, "post", "post/my-post.md", false), qt.IsNotNil) - c.Assert(create.NewContent(h, "post", "post/my-post.md", true), qt.IsNil) - - // from dir - c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil) - c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNotNil) - c.Assert(create.NewContent(h, "my-bundle", "post/my-post", true), qt.IsNil) } func initFs(fs afero.Fs) error { @@ -308,7 +179,7 @@ func initFs(fs afero.Fs) error { afero.WriteFile(fs, filename, []byte(`--- title: Test --- -`), 0666) +`), 0o666) } // create archetype files diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/filesaver.js b/create/skeletons/site/assets/.gitkeep similarity index 100% rename from docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/filesaver.js rename to create/skeletons/site/assets/.gitkeep diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/exclamation.svg b/create/skeletons/site/content/.gitkeep similarity index 100% rename from docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/exclamation.svg rename to create/skeletons/site/content/.gitkeep diff --git a/create/skeletons/site/data/.gitkeep b/create/skeletons/site/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/site/i18n/.gitkeep b/create/skeletons/site/i18n/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/site/layouts/.gitkeep b/create/skeletons/site/layouts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/site/static/.gitkeep b/create/skeletons/site/static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/site/themes/.gitkeep b/create/skeletons/site/themes/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/skeletons.go b/create/skeletons/skeletons.go new file mode 100644 index 000000000..a6241ef92 --- /dev/null +++ b/create/skeletons/skeletons.go @@ -0,0 +1,182 @@ +// Copyright 2024 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 skeletons + +import ( + "bytes" + "embed" + "errors" + "io/fs" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/parser" + "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/spf13/afero" +) + +//go:embed all:site/* +var siteFs embed.FS + +//go:embed all:theme/* +var themeFs embed.FS + +// CreateTheme creates a theme skeleton. +func CreateTheme(createpath string, sourceFs afero.Fs, format string) error { + if exists, _ := helpers.Exists(createpath, sourceFs); exists { + return errors.New(createpath + " already exists") + } + + format = strings.ToLower(format) + + siteConfig := map[string]any{ + "baseURL": "https://example.org/", + "languageCode": "en-US", + "title": "My New Hugo Site", + "menus": map[string]any{ + "main": []any{ + map[string]any{ + "name": "Home", + "pageRef": "/", + "weight": 10, + }, + map[string]any{ + "name": "Posts", + "pageRef": "/posts", + "weight": 20, + }, + map[string]any{ + "name": "Tags", + "pageRef": "/tags", + "weight": 30, + }, + }, + }, + "module": map[string]any{ + "hugoVersion": map[string]any{ + "extended": false, + "min": "0.146.0", + }, + }, + } + + err := createSiteConfig(sourceFs, createpath, siteConfig, format) + if err != nil { + return err + } + + defaultArchetype := map[string]any{ + "title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}", + "date": "{{ .Date }}", + "draft": true, + } + + err = createDefaultArchetype(sourceFs, createpath, defaultArchetype, format) + if err != nil { + return err + } + + return copyFiles(createpath, sourceFs, themeFs) +} + +// CreateSite creates a site skeleton. +func CreateSite(createpath string, sourceFs afero.Fs, force bool, format string) error { + format = strings.ToLower(format) + if exists, _ := helpers.Exists(createpath, sourceFs); exists { + if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir { + return errors.New(createpath + " already exists but not a directory") + } + + isEmpty, _ := helpers.IsEmpty(createpath, sourceFs) + + switch { + case !isEmpty && !force: + return errors.New(createpath + " already exists and is not empty. See --force.") + case !isEmpty && force: + var all []string + fs.WalkDir(siteFs, ".", func(path string, d fs.DirEntry, err error) error { + if d.IsDir() && path != "." { + all = append(all, path) + } + return nil + }) + all = append(all, filepath.Join(createpath, "hugo."+format)) + for _, path := range all { + if exists, _ := helpers.Exists(path, sourceFs); exists { + return errors.New(path + " already exists") + } + } + } + } + + siteConfig := map[string]any{ + "baseURL": "https://example.org/", + "title": "My New Hugo Site", + "languageCode": "en-us", + } + + err := createSiteConfig(sourceFs, createpath, siteConfig, format) + if err != nil { + return err + } + + defaultArchetype := map[string]any{ + "title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}", + "date": "{{ .Date }}", + "draft": true, + } + + err = createDefaultArchetype(sourceFs, createpath, defaultArchetype, format) + if err != nil { + return err + } + + return copyFiles(createpath, sourceFs, siteFs) +} + +func copyFiles(createpath string, sourceFs afero.Fs, skeleton embed.FS) error { + return fs.WalkDir(skeleton, ".", func(path string, d fs.DirEntry, err error) error { + _, slug, _ := strings.Cut(path, "/") + if d.IsDir() { + return sourceFs.MkdirAll(filepath.Join(createpath, slug), 0o777) + } else { + if filepath.Base(path) != ".gitkeep" { + data, _ := fs.ReadFile(skeleton, path) + return helpers.WriteToDisk(filepath.Join(createpath, slug), bytes.NewReader(data), sourceFs) + } + return nil + } + }) +} + +func createSiteConfig(fs afero.Fs, createpath string, in map[string]any, format string) (err error) { + var buf bytes.Buffer + err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(format), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(createpath, "hugo."+format), &buf, fs) +} + +func createDefaultArchetype(fs afero.Fs, createpath string, in map[string]any, format string) (err error) { + var buf bytes.Buffer + err = parser.InterfaceToFrontMatter(in, metadecoders.FormatFromString(format), &buf) + if err != nil { + return err + } + + return helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), &buf, fs) +} diff --git a/create/skeletons/theme/assets/css/main.css b/create/skeletons/theme/assets/css/main.css new file mode 100644 index 000000000..166ade924 --- /dev/null +++ b/create/skeletons/theme/assets/css/main.css @@ -0,0 +1,22 @@ +body { + color: #222; + font-family: sans-serif; + line-height: 1.5; + margin: 1rem; + max-width: 768px; +} + +header { + border-bottom: 1px solid #222; + margin-bottom: 1rem; +} + +footer { + border-top: 1px solid #222; + margin-top: 1rem; +} + +a { + color: #00e; + text-decoration: none; +} diff --git a/create/skeletons/theme/assets/js/main.js b/create/skeletons/theme/assets/js/main.js new file mode 100644 index 000000000..e2aac5275 --- /dev/null +++ b/create/skeletons/theme/assets/js/main.js @@ -0,0 +1 @@ +console.log('This site was generated by Hugo.'); diff --git a/create/skeletons/theme/content/_index.md b/create/skeletons/theme/content/_index.md new file mode 100644 index 000000000..652623b57 --- /dev/null +++ b/create/skeletons/theme/content/_index.md @@ -0,0 +1,9 @@ ++++ +title = 'Home' +date = 2023-01-01T08:00:00-07:00 +draft = false ++++ + +Laborum voluptate pariatur ex culpa magna nostrud est incididunt fugiat +pariatur do dolor ipsum enim. Consequat tempor do dolor eu. Non id id anim anim +excepteur excepteur pariatur nostrud qui irure ullamco. diff --git a/create/skeletons/theme/content/posts/_index.md b/create/skeletons/theme/content/posts/_index.md new file mode 100644 index 000000000..e7066c092 --- /dev/null +++ b/create/skeletons/theme/content/posts/_index.md @@ -0,0 +1,7 @@ ++++ +title = 'Posts' +date = 2023-01-01T08:30:00-07:00 +draft = false ++++ + +Tempor est exercitation ad qui pariatur quis adipisicing aliquip nisi ea consequat ipsum occaecat. Nostrud consequat ullamco laboris fugiat esse esse adipisicing velit laborum ipsum incididunt ut enim. Dolor pariatur nulla quis fugiat dolore excepteur. Aliquip ad quis aliqua enim do consequat. diff --git a/create/skeletons/theme/content/posts/post-1.md b/create/skeletons/theme/content/posts/post-1.md new file mode 100644 index 000000000..3e3fc6b25 --- /dev/null +++ b/create/skeletons/theme/content/posts/post-1.md @@ -0,0 +1,10 @@ ++++ +title = 'Post 1' +date = 2023-01-15T09:00:00-07:00 +draft = false +tags = ['red'] ++++ + +Tempor proident minim aliquip reprehenderit dolor et ad anim Lorem duis sint eiusmod. Labore ut ea duis dolor. Incididunt consectetur proident qui occaecat incididunt do nisi Lorem. Tempor do laborum elit laboris excepteur eiusmod do. Eiusmod nisi excepteur ut amet pariatur adipisicing Lorem. + +Occaecat nulla excepteur dolore excepteur duis eiusmod ullamco officia anim in voluptate ea occaecat officia. Cillum sint esse velit ea officia minim fugiat. Elit ea esse id aliquip pariatur cupidatat id duis minim incididunt ea ea. Anim ut duis sunt nisi. Culpa cillum sit voluptate voluptate eiusmod dolor. Enim nisi Lorem ipsum irure est excepteur voluptate eu in enim nisi. Nostrud ipsum Lorem anim sint labore consequat do. diff --git a/create/skeletons/theme/content/posts/post-2.md b/create/skeletons/theme/content/posts/post-2.md new file mode 100644 index 000000000..22b828769 --- /dev/null +++ b/create/skeletons/theme/content/posts/post-2.md @@ -0,0 +1,10 @@ ++++ +title = 'Post 2' +date = 2023-02-15T10:00:00-07:00 +draft = false +tags = ['red','green'] ++++ + +Anim eiusmod irure incididunt sint cupidatat. Incididunt irure irure irure nisi ipsum do ut quis fugiat consectetur proident cupidatat incididunt cillum. Dolore voluptate occaecat qui mollit laborum ullamco et. Ipsum laboris officia anim laboris culpa eiusmod ex magna ex cupidatat anim ipsum aute. Mollit aliquip occaecat qui sunt velit ut cupidatat reprehenderit enim sunt laborum. Velit veniam in officia nulla adipisicing ut duis officia. + +Exercitation voluptate irure in irure tempor mollit Lorem nostrud ad officia. Velit id fugiat occaecat do tempor. Sit officia Lorem aliquip eu deserunt consectetur. Aute proident deserunt in nulla aliquip dolore ipsum Lorem ut cupidatat consectetur sit sint laborum. Esse cupidatat sit sint sunt tempor exercitation deserunt. Labore dolor duis laborum est do nisi ut veniam dolor et nostrud nostrud. diff --git a/create/skeletons/theme/content/posts/post-3/bryce-canyon.jpg b/create/skeletons/theme/content/posts/post-3/bryce-canyon.jpg new file mode 100644 index 000000000..9a923bea0 Binary files /dev/null and b/create/skeletons/theme/content/posts/post-3/bryce-canyon.jpg differ diff --git a/create/skeletons/theme/content/posts/post-3/index.md b/create/skeletons/theme/content/posts/post-3/index.md new file mode 100644 index 000000000..ca42a664b --- /dev/null +++ b/create/skeletons/theme/content/posts/post-3/index.md @@ -0,0 +1,12 @@ ++++ +title = 'Post 3' +date = 2023-03-15T11:00:00-07:00 +draft = false +tags = ['red','green','blue'] ++++ + +Occaecat aliqua consequat laborum ut ex aute aliqua culpa quis irure esse magna dolore quis. Proident fugiat labore eu laboris officia Lorem enim. Ipsum occaecat cillum ut tempor id sint aliqua incididunt nisi incididunt reprehenderit. Voluptate ad minim sint est aute aliquip esse occaecat tempor officia qui sunt. Aute ex ipsum id ut in est velit est laborum incididunt. Aliqua qui id do esse sunt eiusmod id deserunt eu nostrud aute sit ipsum. Deserunt esse cillum Lorem non magna adipisicing mollit amet consequat. + +![Bryce Canyon National Park](bryce-canyon.jpg) + +Sit excepteur do velit veniam mollit in nostrud laboris incididunt ea. Amet eu cillum ut reprehenderit culpa aliquip labore laborum amet sit sit duis. Laborum id proident nostrud dolore laborum reprehenderit quis mollit nulla amet veniam officia id id. Aliquip in deserunt qui magna duis qui pariatur officia sunt deserunt. diff --git a/create/skeletons/theme/data/.gitkeep b/create/skeletons/theme/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/theme/i18n/.gitkeep b/create/skeletons/theme/i18n/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/create/skeletons/theme/layouts/_partials/footer.html b/create/skeletons/theme/layouts/_partials/footer.html new file mode 100644 index 000000000..a7cd916d0 --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/footer.html @@ -0,0 +1 @@ +

Copyright {{ now.Year }}. All rights reserved.

diff --git a/create/skeletons/theme/layouts/_partials/head.html b/create/skeletons/theme/layouts/_partials/head.html new file mode 100644 index 000000000..02c224018 --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/head.html @@ -0,0 +1,5 @@ + + +{{ if .IsHome }}{{ site.Title }}{{ else }}{{ printf "%s | %s" .Title site.Title }}{{ end }} +{{ partialCached "head/css.html" . }} +{{ partialCached "head/js.html" . }} diff --git a/create/skeletons/theme/layouts/_partials/head/css.html b/create/skeletons/theme/layouts/_partials/head/css.html new file mode 100644 index 000000000..d76d23a16 --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/head/css.html @@ -0,0 +1,9 @@ +{{- with resources.Get "css/main.css" }} + {{- if hugo.IsDevelopment }} + + {{- else }} + {{- with . | minify | fingerprint }} + + {{- end }} + {{- end }} +{{- end }} diff --git a/create/skeletons/theme/layouts/_partials/head/js.html b/create/skeletons/theme/layouts/_partials/head/js.html new file mode 100644 index 000000000..16ffbedfe --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/head/js.html @@ -0,0 +1,16 @@ +{{- with resources.Get "js/main.js" }} + {{- $opts := dict + "minify" (not hugo.IsDevelopment) + "sourceMap" (cond hugo.IsDevelopment "external" "") + "targetPath" "js/main.js" + }} + {{- with . | js.Build $opts }} + {{- if hugo.IsDevelopment }} + + {{- else }} + {{- with . | fingerprint }} + + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/create/skeletons/theme/layouts/_partials/header.html b/create/skeletons/theme/layouts/_partials/header.html new file mode 100644 index 000000000..7980a00e1 --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/header.html @@ -0,0 +1,2 @@ +

{{ site.Title }}

+{{ partial "menu.html" (dict "menuID" "main" "page" .) }} diff --git a/create/skeletons/theme/layouts/_partials/menu.html b/create/skeletons/theme/layouts/_partials/menu.html new file mode 100644 index 000000000..14245b55d --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/menu.html @@ -0,0 +1,51 @@ +{{- /* +Renders a menu for the given menu ID. + +@context {page} page The current page. +@context {string} menuID The menu ID. + +@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $menuID := .menuID }} + +{{- with index site.Menus $menuID }} + +{{- end }} + +{{- define "_partials/inline/menu/walk.html" }} + {{- $page := .page }} + {{- range .menuEntries }} + {{- $attrs := dict "href" .URL }} + {{- if $page.IsMenuCurrent .Menu . }} + {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }} + {{- else if $page.HasMenuCurrent .Menu .}} + {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }} + {{- end }} + {{- $name := .Name }} + {{- with .Identifier }} + {{- with T . }} + {{- $name = . }} + {{- end }} + {{- end }} +
  • + {{ $name }} + {{- with .Children }} +
      + {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }} +
    + {{- end }} +
  • + {{- end }} +{{- end }} diff --git a/create/skeletons/theme/layouts/_partials/terms.html b/create/skeletons/theme/layouts/_partials/terms.html new file mode 100644 index 000000000..8a6ebec2a --- /dev/null +++ b/create/skeletons/theme/layouts/_partials/terms.html @@ -0,0 +1,23 @@ +{{- /* +For a given taxonomy, renders a list of terms assigned to the page. + +@context {page} page The current page. +@context {string} taxonomy The taxonomy. + +@example: {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} +*/}} + +{{- $page := .page }} +{{- $taxonomy := .taxonomy }} + +{{- with $page.GetTerms $taxonomy }} + {{- $label := (index . 0).Parent.LinkTitle }} +
    +
    {{ $label }}:
    + +
    +{{- end }} diff --git a/create/skeletons/theme/layouts/baseof.html b/create/skeletons/theme/layouts/baseof.html new file mode 100644 index 000000000..39dcbec61 --- /dev/null +++ b/create/skeletons/theme/layouts/baseof.html @@ -0,0 +1,17 @@ + + + + {{ partial "head.html" . }} + + +
    + {{ partial "header.html" . }} +
    +
    + {{ block "main" . }}{{ end }} +
    +
    + {{ partial "footer.html" . }} +
    + + diff --git a/create/skeletons/theme/layouts/home.html b/create/skeletons/theme/layouts/home.html new file mode 100644 index 000000000..0df659742 --- /dev/null +++ b/create/skeletons/theme/layouts/home.html @@ -0,0 +1,7 @@ +{{ define "main" }} + {{ .Content }} + {{ range site.RegularPages }} +

    {{ .LinkTitle }}

    + {{ .Summary }} + {{ end }} +{{ end }} diff --git a/create/skeletons/theme/layouts/page.html b/create/skeletons/theme/layouts/page.html new file mode 100644 index 000000000..7e286c802 --- /dev/null +++ b/create/skeletons/theme/layouts/page.html @@ -0,0 +1,10 @@ +{{ define "main" }} +

    {{ .Title }}

    + + {{ $dateMachine := .Date | time.Format "2006-01-02T15:04:05-07:00" }} + {{ $dateHuman := .Date | time.Format ":date_long" }} + + + {{ .Content }} + {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }} +{{ end }} diff --git a/create/skeletons/theme/layouts/section.html b/create/skeletons/theme/layouts/section.html new file mode 100644 index 000000000..50fc92d40 --- /dev/null +++ b/create/skeletons/theme/layouts/section.html @@ -0,0 +1,8 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ .Summary }} + {{ end }} +{{ end }} diff --git a/create/skeletons/theme/layouts/taxonomy.html b/create/skeletons/theme/layouts/taxonomy.html new file mode 100644 index 000000000..c2e787519 --- /dev/null +++ b/create/skeletons/theme/layouts/taxonomy.html @@ -0,0 +1,7 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ end }} +{{ end }} diff --git a/create/skeletons/theme/layouts/term.html b/create/skeletons/theme/layouts/term.html new file mode 100644 index 000000000..c2e787519 --- /dev/null +++ b/create/skeletons/theme/layouts/term.html @@ -0,0 +1,7 @@ +{{ define "main" }} +

    {{ .Title }}

    + {{ .Content }} + {{ range .Pages }} +

    {{ .LinkTitle }}

    + {{ end }} +{{ end }} diff --git a/create/skeletons/theme/static/favicon.ico b/create/skeletons/theme/static/favicon.ico new file mode 100644 index 000000000..67f8b7778 Binary files /dev/null and b/create/skeletons/theme/static/favicon.ico differ diff --git a/deploy/cloudfront.go b/deploy/cloudfront.go index 2f6d94b18..3202a73ea 100644 --- a/deploy/cloudfront.go +++ b/deploy/cloudfront.go @@ -11,44 +11,62 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy +//go:build withdeploy package deploy import ( "context" + "net/url" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/cloudfront" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudfront" + "github.com/aws/aws-sdk-go-v2/service/cloudfront/types" + "github.com/gohugoio/hugo/deploy/deployconfig" + gcaws "gocloud.dev/aws" ) +// V2ConfigFromURLParams will fail for any unknown params, so we need to remove them. +// This is a mysterious API, but inspecting the code the known params are: +var v2ConfigValidParams = map[string]bool{ + "endpoint": true, + "region": true, + "profile": true, + "awssdk": true, +} + // InvalidateCloudFront invalidates the CloudFront cache for distributionID. -// It uses the default AWS credentials from the environment. -func InvalidateCloudFront(ctx context.Context, distributionID string) error { - // SharedConfigEnable enables loading "shared config (~/.aws/config) and - // shared credentials (~/.aws/credentials) files". - // See https://docs.aws.amazon.com/sdk-for-go/api/aws/session/ for more - // details. - // This is the same codepath used by Go CDK when creating an s3 URL. - // TODO: Update this to a Go CDK helper once available - // (https://github.com/google/go-cloud/issues/2003). - sess, err := session.NewSessionWithOptions(session.Options{SharedConfigState: session.SharedConfigEnable}) +// Uses AWS credentials config from the bucket URL. +func InvalidateCloudFront(ctx context.Context, target *deployconfig.Target) error { + u, err := url.Parse(target.URL) if err != nil { return err } + vals := u.Query() + + // Remove any unknown params. + for k := range vals { + if !v2ConfigValidParams[k] { + vals.Del(k) + } + } + + cfg, err := gcaws.V2ConfigFromURLParams(ctx, vals) + if err != nil { + return err + } + cf := cloudfront.NewFromConfig(cfg) req := &cloudfront.CreateInvalidationInput{ - DistributionId: aws.String(distributionID), - InvalidationBatch: &cloudfront.InvalidationBatch{ + DistributionId: aws.String(target.CloudFrontDistributionID), + InvalidationBatch: &types.InvalidationBatch{ CallerReference: aws.String(time.Now().Format("20060102150405")), - Paths: &cloudfront.Paths{ - Items: []*string{aws.String("/*")}, - Quantity: aws.Int64(1), + Paths: &types.Paths{ + Items: []string{"/*"}, + Quantity: aws.Int32(1), }, }, } - _, err = cloudfront.New(sess).CreateInvalidationWithContext(ctx, req) + _, err = cf.CreateInvalidation(ctx, req) return err } diff --git a/deploy/deploy.go b/deploy/deploy.go index db88996a9..57e1f41a2 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -11,8 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy +//go:build withdeploy package deploy @@ -22,6 +21,7 @@ import ( "context" "crypto/md5" "encoding/hex" + "errors" "fmt" "io" "mime" @@ -33,14 +33,14 @@ import ( "strings" "sync" - "errors" - "github.com/dustin/go-humanize" "github.com/gobwas/glob" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/common/para" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deploy/deployconfig" "github.com/gohugoio/hugo/media" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" "golang.org/x/text/unicode/norm" "gocloud.dev/blob" @@ -56,11 +56,12 @@ type Deployer struct { bucket *blob.Bucket mediaTypes media.Types // Hugo's MediaType to guess ContentType - quiet bool // true reduces STDOUT + quiet bool // true reduces STDOUT // TODO(bep) remove, this is a global feature. - cfg DeployConfig + cfg deployconfig.DeployConfig + logger loggers.Logger - target *Target // the target to deploy to + target *deployconfig.Target // the target to deploy to // For tests... summary deploySummary // summary of latest Deploy results @@ -73,9 +74,8 @@ type deploySummary struct { const metaMD5Hash = "md5chksum" // the meta key to store md5hash in // New constructs a new *Deployer. -func New(cfg config.AllProvider, localFs afero.Fs) (*Deployer, error) { - - dcfg := cfg.GetConfigSection(deploymentConfigKey).(DeployConfig) +func New(cfg config.AllProvider, logger loggers.Logger, localFs afero.Fs) (*Deployer, error) { + dcfg := cfg.GetConfigSection(deployconfig.DeploymentConfigKey).(deployconfig.DeployConfig) targetName := dcfg.Target if len(dcfg.Targets) == 0 { @@ -84,7 +84,7 @@ func New(cfg config.AllProvider, localFs afero.Fs) (*Deployer, error) { mediaTypes := cfg.GetConfigSection("mediaTypes").(media.Types) // Find the target to deploy to. - var tgt *Target + var tgt *deployconfig.Target if targetName == "" { // Default to the first target. tgt = dcfg.Targets[0] @@ -112,12 +112,16 @@ func (d *Deployer) openBucket(ctx context.Context) (*blob.Bucket, error) { if d.bucket != nil { return d.bucket, nil } - jww.FEEDBACK.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL) + d.logger.Printf("Deploying to target %q (%s)\n", d.target.Name, d.target.URL) return blob.OpenBucket(ctx, d.target.URL) } // Deploy deploys the site to a target. func (d *Deployer) Deploy(ctx context.Context) error { + if d.logger == nil { + d.logger = loggers.NewDefault() + } + bucket, err := d.openBucket(ctx) if err != nil { return err @@ -129,36 +133,40 @@ func (d *Deployer) Deploy(ctx context.Context) error { // Load local files from the source directory. var include, exclude glob.Glob + var mappath func(string) string if d.target != nil { - include, exclude = d.target.includeGlob, d.target.excludeGlob + include, exclude = d.target.IncludeGlob, d.target.ExcludeGlob + if d.target.StripIndexHTML { + mappath = stripIndexHTML + } } - local, err := walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes) + local, err := d.walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes, mappath) if err != nil { return err } - jww.INFO.Printf("Found %d local files.\n", len(local)) + d.logger.Infof("Found %d local files.\n", len(local)) d.summary.NumLocal = len(local) // Load remote files from the target. - remote, err := walkRemote(ctx, bucket, include, exclude) + remote, err := d.walkRemote(ctx, bucket, include, exclude) if err != nil { return err } - jww.INFO.Printf("Found %d remote files.\n", len(remote)) + d.logger.Infof("Found %d remote files.\n", len(remote)) d.summary.NumRemote = len(remote) // Diff local vs remote to see what changes need to be applied. - uploads, deletes := findDiffs(local, remote, d.cfg.Force) + uploads, deletes := d.findDiffs(local, remote, d.cfg.Force) d.summary.NumUploads = len(uploads) d.summary.NumDeletes = len(deletes) if len(uploads)+len(deletes) == 0 { if !d.quiet { - jww.FEEDBACK.Println("No changes required.") + d.logger.Println("No changes required.") } return nil } if !d.quiet { - jww.FEEDBACK.Println(summarizeChanges(uploads, deletes)) + d.logger.Println(summarizeChanges(uploads, deletes)) } // Ask for confirmation before proceeding. @@ -175,7 +183,7 @@ func (d *Deployer) Deploy(ctx context.Context) error { // Order the uploads. They are organized in groups; all uploads in a group // must be complete before moving on to the next group. - uploadGroups := applyOrdering(d.cfg.ordering, uploads) + uploadGroups := applyOrdering(d.cfg.Ordering, uploads) nParallel := d.cfg.Workers var errs []error @@ -192,14 +200,14 @@ func (d *Deployer) Deploy(ctx context.Context) error { for _, upload := range uploads { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload) + d.logger.Printf("[DRY RUN] Would upload: %v\n", upload) } continue } sem <- struct{}{} go func(upload *fileToUpload) { - if err := doSingleUpload(ctx, bucket, upload); err != nil { + if err := d.doSingleUpload(ctx, bucket, upload); err != nil { errMu.Lock() defer errMu.Unlock() errs = append(errs, err) @@ -214,7 +222,7 @@ func (d *Deployer) Deploy(ctx context.Context) error { } if d.cfg.MaxDeletes != -1 && len(deletes) > d.cfg.MaxDeletes { - jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.cfg.MaxDeletes) + d.logger.Warnf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.cfg.MaxDeletes) d.summary.NumDeletes = 0 } else { // Apply deletes in parallel. @@ -223,16 +231,16 @@ func (d *Deployer) Deploy(ctx context.Context) error { for _, del := range deletes { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del) + d.logger.Printf("[DRY RUN] Would delete %s\n", del) } continue } sem <- struct{}{} go func(del string) { - jww.INFO.Printf("Deleting %s...\n", del) + d.logger.Infof("Deleting %s...\n", del) if err := bucket.Delete(ctx, del); err != nil { if gcerrors.Code(err) == gcerrors.NotFound { - jww.WARN.Printf("Failed to delete %q because it wasn't found: %v", del, err) + d.logger.Warnf("Failed to delete %q because it wasn't found: %v", del, err) } else { errMu.Lock() defer errMu.Unlock() @@ -250,24 +258,24 @@ func (d *Deployer) Deploy(ctx context.Context) error { if len(errs) > 0 { if !d.quiet { - jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs)) + d.logger.Printf("Encountered %d errors.\n", len(errs)) } return errs[0] } if !d.quiet { - jww.FEEDBACK.Println("Success!") + d.logger.Println("Success!") } if d.cfg.InvalidateCDN { if d.target.CloudFrontDistributionID != "" { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID) + d.logger.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID) } } else { - jww.FEEDBACK.Println("Invalidating CloudFront CDN...") - if err := InvalidateCloudFront(ctx, d.target.CloudFrontDistributionID); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate CloudFront CDN: %v\n", err) + d.logger.Println("Invalidating CloudFront CDN...") + if err := InvalidateCloudFront(ctx, d.target); err != nil { + d.logger.Printf("Failed to invalidate CloudFront CDN: %v\n", err) return err } } @@ -275,17 +283,17 @@ func (d *Deployer) Deploy(ctx context.Context) error { if d.target.GoogleCloudCDNOrigin != "" { if d.cfg.DryRun { if !d.quiet { - jww.FEEDBACK.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin) + d.logger.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin) } } else { - jww.FEEDBACK.Println("Invalidating Google Cloud CDN...") + d.logger.Println("Invalidating Google Cloud CDN...") if err := InvalidateGoogleCloudCDN(ctx, d.target.GoogleCloudCDNOrigin); err != nil { - jww.FEEDBACK.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) + d.logger.Printf("Failed to invalidate Google Cloud CDN: %v\n", err) return err } } } - jww.FEEDBACK.Println("Success!") + d.logger.Println("Success!") } return nil } @@ -300,8 +308,8 @@ func summarizeChanges(uploads []*fileToUpload, deletes []string) string { } // doSingleUpload executes a single file upload. -func doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error { - jww.INFO.Printf("Uploading %v...\n", upload) +func (d *Deployer) doSingleUpload(ctx context.Context, bucket *blob.Bucket, upload *fileToUpload) error { + d.logger.Infof("Uploading %v...\n", upload) opts := &blob.WriterOptions{ CacheControl: upload.Local.CacheControl(), ContentEncoding: upload.Local.ContentEncoding(), @@ -340,14 +348,14 @@ type localFile struct { UploadSize int64 fs afero.Fs - matcher *Matcher + matcher *deployconfig.Matcher md5 []byte // cache gzipped bytes.Buffer // cached of gzipped contents if gzipping mediaTypes media.Types } // newLocalFile initializes a *localFile. -func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *Matcher, mt media.Types) (*localFile, error) { +func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *deployconfig.Matcher, mt media.Types) (*localFile, error) { f, err := fs.Open(nativePath) if err != nil { return nil, err @@ -479,8 +487,13 @@ func knownHiddenDirectory(name string) bool { // walkLocal walks the source directory and returns a flat list of files, // using localFile.SlashPath as the map keys. -func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) { - retval := map[string]*localFile{} +func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, include, exclude glob.Glob, mediaTypes media.Types, mappath func(string) string) (map[string]*localFile, error) { + retval := make(map[string]*localFile) + var mu sync.Mutex + + workers := para.New(d.cfg.Workers) + g, _ := workers.Start(context.Background()) + err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -501,45 +514,68 @@ func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, med return nil } - // When a file system is HFS+, its filepath is in NFD form. - if runtime.GOOS == "darwin" { - path = norm.NFC.String(path) - } - - // Check include/exclude matchers. - slashpath := filepath.ToSlash(path) - if include != nil && !include.Match(slashpath) { - jww.INFO.Printf(" dropping %q due to include\n", slashpath) - return nil - } - if exclude != nil && exclude.Match(slashpath) { - jww.INFO.Printf(" dropping %q due to exclude\n", slashpath) - return nil - } - - // Find the first matching matcher (if any). - var m *Matcher - for _, cur := range matchers { - if cur.Matches(slashpath) { - m = cur - break + // Process each file in a worker + g.Run(func() error { + // When a file system is HFS+, its filepath is in NFD form. + if runtime.GOOS == "darwin" { + path = norm.NFC.String(path) } - } - lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes) - if err != nil { - return err - } - retval[lf.SlashPath] = lf + + // Check include/exclude matchers. + slashpath := filepath.ToSlash(path) + if include != nil && !include.Match(slashpath) { + d.logger.Infof(" dropping %q due to include\n", slashpath) + return nil + } + if exclude != nil && exclude.Match(slashpath) { + d.logger.Infof(" dropping %q due to exclude\n", slashpath) + return nil + } + + // Find the first matching matcher (if any). + var m *deployconfig.Matcher + for _, cur := range matchers { + if cur.Matches(slashpath) { + m = cur + break + } + } + // Apply any additional modifications to the local path, to map it to + // the remote path. + if mappath != nil { + slashpath = mappath(slashpath) + } + lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes) + if err != nil { + return err + } + mu.Lock() + retval[lf.SlashPath] = lf + mu.Unlock() + return nil + }) return nil }) if err != nil { return nil, err } + if err := g.Wait(); err != nil { + return nil, err + } return retval, nil } +// stripIndexHTML remaps keys matching "/index.html" to "/". +func stripIndexHTML(slashpath string) string { + const suffix = "/index.html" + if strings.HasSuffix(slashpath, suffix) { + return slashpath[:len(slashpath)-len(suffix)+1] + } + return slashpath +} + // walkRemote walks the target bucket and returns a flat list. -func walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob.Glob) (map[string]*blob.ListObject, error) { +func (d *Deployer) walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob.Glob) (map[string]*blob.ListObject, error) { retval := map[string]*blob.ListObject{} iter := bucket.List(nil) for { @@ -552,11 +588,11 @@ func walkRemote(ctx context.Context, bucket *blob.Bucket, include, exclude glob. } // Check include/exclude matchers. if include != nil && !include.Match(obj.Key) { - jww.INFO.Printf(" remote dropping %q due to include\n", obj.Key) + d.logger.Infof(" remote dropping %q due to include\n", obj.Key) continue } if exclude != nil && exclude.Match(obj.Key) { - jww.INFO.Printf(" remote dropping %q due to exclude\n", obj.Key) + d.logger.Infof(" remote dropping %q due to exclude\n", obj.Key) continue } // If the remote didn't give us an MD5, use remote attributes MD5, if that doesn't exist compute one. @@ -629,7 +665,7 @@ func (u *fileToUpload) String() string { // findDiffs diffs localFiles vs remoteFiles to see what changes should be // applied to the remote target. It returns a slice of *fileToUpload and a // slice of paths for files to delete. -func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) { +func (d *Deployer) findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.ListObject, force bool) ([]*fileToUpload, []string) { var uploads []*fileToUpload var deletes []string @@ -670,8 +706,6 @@ func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.Li } else if !bytes.Equal(lf.MD5(), remoteFile.MD5) { upload = true reason = reasonMD5Differs - } else { - // Nope! Leave uploaded = false. } found[path] = true } else { @@ -680,10 +714,10 @@ func findDiffs(localFiles map[string]*localFile, remoteFiles map[string]*blob.Li reason = reasonNotFound } if upload { - jww.DEBUG.Printf("%s needs to be uploaded: %v\n", path, reason) + d.logger.Debugf("%s needs to be uploaded: %v\n", path, reason) uploads = append(uploads, &fileToUpload{lf, reason}) } else { - jww.DEBUG.Printf("%s exists at target and does not need to be uploaded", path) + d.logger.Debugf("%s exists at target and does not need to be uploaded", path) } } diff --git a/deploy/deploy_azure.go b/deploy/deploy_azure.go index fc7daca3b..b1ce7358c 100644 --- a/deploy/deploy_azure.go +++ b/deploy/deploy_azure.go @@ -11,8 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !solaris && !nodeploy -// +build !solaris,!nodeploy +//go:build !solaris && withdeploy package deploy diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go index fe874fbbd..bdc8299a0 100644 --- a/deploy/deploy_test.go +++ b/deploy/deploy_test.go @@ -11,8 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy +//go:build withdeploy package deploy @@ -30,6 +29,9 @@ import ( "sort" "testing" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/deploy/deployconfig" + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/media" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -108,7 +110,7 @@ func TestFindDiffs(t *testing.T) { { Description: "local == remote with route.Force true -> diffs", Local: []*localFile{ - {NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &Matcher{Force: true}, md5: hash1}, + {NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &deployconfig.Matcher{Force: true}, md5: hash1}, makeLocal("bbb", 2, hash1), }, Remote: []*blob.ListObject{ @@ -197,7 +199,8 @@ func TestFindDiffs(t *testing.T) { for _, r := range tc.Remote { remote[r.Key] = r } - gotUpdates, gotDeletes := findDiffs(local, remote, tc.Force) + d := newDeployer() + gotUpdates, gotDeletes := d.findDiffs(local, remote, tc.Force) gotUpdates = applyOrdering(nil, gotUpdates)[0] sort.Slice(gotDeletes, func(i, j int) bool { return gotDeletes[i] < gotDeletes[j] }) if diff := cmp.Diff(gotUpdates, tc.WantUpdates, cmpopts.IgnoreUnexported(localFile{})); diff != "" { @@ -212,8 +215,9 @@ func TestFindDiffs(t *testing.T) { func TestWalkLocal(t *testing.T) { tests := map[string]struct { - Given []string - Expect []string + Given []string + Expect []string + MapPath func(string) string }{ "Empty": { Given: []string{}, @@ -231,6 +235,11 @@ func TestWalkLocal(t *testing.T) { Given: []string{"file.txt", ".hidden_dir/file.txt", ".well-known/file.txt"}, Expect: []string{"file.txt", ".well-known/file.txt"}, }, + "StripIndexHTML": { + Given: []string{"index.html", "file.txt", "dir/index.html", "dir/file.txt"}, + Expect: []string{"index.html", "file.txt", "dir/", "dir/file.txt"}, + MapPath: stripIndexHTML, + }, } for desc, tc := range tests { @@ -239,7 +248,7 @@ func TestWalkLocal(t *testing.T) { for _, name := range tc.Given { dir, _ := path.Split(name) if dir != "" { - if err := fs.MkdirAll(dir, 0755); err != nil { + if err := fs.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } } @@ -249,7 +258,8 @@ func TestWalkLocal(t *testing.T) { fd.Close() } } - if got, err := walkLocal(fs, nil, nil, nil, media.DefaultTypes); err != nil { + d := newDeployer() + if got, err := d.walkLocal(fs, nil, nil, nil, media.DefaultTypes, tc.MapPath); err != nil { t.Fatal(err) } else { expect := map[string]any{} @@ -269,6 +279,63 @@ func TestWalkLocal(t *testing.T) { } } +func TestStripIndexHTML(t *testing.T) { + tests := map[string]struct { + Input string + Output string + }{ + "Unmapped": {Input: "normal_file.txt", Output: "normal_file.txt"}, + "Stripped": {Input: "directory/index.html", Output: "directory/"}, + "NoSlash": {Input: "prefix_index.html", Output: "prefix_index.html"}, + "Root": {Input: "index.html", Output: "index.html"}, + } + for desc, tc := range tests { + t.Run(desc, func(t *testing.T) { + got := stripIndexHTML(tc.Input) + if got != tc.Output { + t.Errorf("got %q, expect %q", got, tc.Output) + } + }) + } +} + +func TestStripIndexHTMLMatcher(t *testing.T) { + // StripIndexHTML should not affect matchers. + fs := afero.NewMemMapFs() + if err := fs.Mkdir("dir", 0o755); err != nil { + t.Fatal(err) + } + for _, name := range []string{"index.html", "dir/index.html", "file.txt"} { + if fd, err := fs.Create(name); err != nil { + t.Fatal(err) + } else { + fd.Close() + } + } + d := newDeployer() + const pattern = `\.html$` + matcher := &deployconfig.Matcher{Pattern: pattern, Gzip: true, Re: regexp.MustCompile(pattern)} + if got, err := d.walkLocal(fs, []*deployconfig.Matcher{matcher}, nil, nil, media.DefaultTypes, stripIndexHTML); err != nil { + t.Fatal(err) + } else { + for _, name := range []string{"index.html", "dir/"} { + lf := got[name] + if lf == nil { + t.Errorf("missing file %q", name) + } else if lf.matcher == nil { + t.Errorf("file %q has nil matcher, expect %q", name, pattern) + } + } + const name = "file.txt" + lf := got[name] + if lf == nil { + t.Errorf("missing file %q", name) + } else if lf.matcher != nil { + t.Errorf("file %q has matcher %q, expect nil", name, lf.matcher.Pattern) + } + } +} + func TestLocalFile(t *testing.T) { const ( content = "hello world!" @@ -289,7 +356,7 @@ func TestLocalFile(t *testing.T) { tests := []struct { Description string Path string - Matcher *Matcher + Matcher *deployconfig.Matcher MediaTypesConfig map[string]any WantContent []byte WantSize int64 @@ -315,7 +382,7 @@ func TestLocalFile(t *testing.T) { { Description: "CacheControl from matcher", Path: "foo.txt", - Matcher: &Matcher{CacheControl: "max-age=630720000"}, + Matcher: &deployconfig.Matcher{CacheControl: "max-age=630720000"}, WantContent: contentBytes, WantSize: contentLen, WantMD5: contentMD5[:], @@ -324,7 +391,7 @@ func TestLocalFile(t *testing.T) { { Description: "ContentEncoding from matcher", Path: "foo.txt", - Matcher: &Matcher{ContentEncoding: "foobar"}, + Matcher: &deployconfig.Matcher{ContentEncoding: "foobar"}, WantContent: contentBytes, WantSize: contentLen, WantMD5: contentMD5[:], @@ -333,7 +400,7 @@ func TestLocalFile(t *testing.T) { { Description: "ContentType from matcher", Path: "foo.txt", - Matcher: &Matcher{ContentType: "foo/bar"}, + Matcher: &deployconfig.Matcher{ContentType: "foo/bar"}, WantContent: contentBytes, WantSize: contentLen, WantMD5: contentMD5[:], @@ -342,7 +409,7 @@ func TestLocalFile(t *testing.T) { { Description: "gzipped content", Path: "foo.txt", - Matcher: &Matcher{Gzip: true}, + Matcher: &deployconfig.Matcher{Gzip: true}, WantContent: gzBytes, WantSize: gzLen, WantMD5: gzMD5[:], @@ -527,7 +594,7 @@ func initFsTests(t *testing.T) []*fsTest { membucket := memblob.OpenBucket(nil) t.Cleanup(func() { membucket.Close() }) - filefs := afero.NewBasePathFs(afero.NewOsFs(), tmpfsdir) + filefs := hugofs.NewBasePathFs(afero.NewOsFs(), tmpfsdir) filebucket, err := fileblob.OpenBucket(tmpbucketdir, nil) if err != nil { t.Fatal(err) @@ -556,7 +623,7 @@ func TestEndToEndSync(t *testing.T) { localFs: test.fs, bucket: test.bucket, mediaTypes: media.DefaultTypes, - cfg: DeployConfig{MaxDeletes: -1}, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, } // Initial deployment should sync remote with local. @@ -639,7 +706,7 @@ func TestMaxDeletes(t *testing.T) { localFs: test.fs, bucket: test.bucket, mediaTypes: media.DefaultTypes, - cfg: DeployConfig{MaxDeletes: -1}, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, } // Sync remote with local. @@ -760,16 +827,16 @@ func TestIncludeExclude(t *testing.T) { if err != nil { t.Fatal(err) } - tgt := &Target{ + tgt := &deployconfig.Target{ Include: test.Include, Exclude: test.Exclude, } - if err := tgt.parseIncludeExclude(); err != nil { + if err := tgt.ParseIncludeExclude(); err != nil { t.Error(err) } deployer := &Deployer{ localFs: fsTest.fs, - cfg: DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket, target: tgt, mediaTypes: media.DefaultTypes, } @@ -826,7 +893,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) { } deployer := &Deployer{ localFs: fsTest.fs, - cfg: DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket, mediaTypes: media.DefaultTypes, } @@ -844,11 +911,11 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) { } // Second sync - tgt := &Target{ + tgt := &deployconfig.Target{ Include: test.Include, Exclude: test.Exclude, } - if err := tgt.parseIncludeExclude(); err != nil { + if err := tgt.ParseIncludeExclude(); err != nil { t.Error(err) } deployer.target = tgt @@ -878,7 +945,7 @@ func TestCompression(t *testing.T) { deployer := &Deployer{ localFs: test.fs, bucket: test.bucket, - cfg: DeployConfig{MaxDeletes: -1, Matchers: []*Matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}}}, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: ".*", Gzip: true, Re: regexp.MustCompile(".*")}}}, mediaTypes: media.DefaultTypes, } @@ -933,7 +1000,7 @@ func TestMatching(t *testing.T) { deployer := &Deployer{ localFs: test.fs, bucket: test.bucket, - cfg: DeployConfig{MaxDeletes: -1, Matchers: []*Matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}}}, + cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: "^subdir/aaa$", Force: true, Re: regexp.MustCompile("^subdir/aaa$")}}}, mediaTypes: media.DefaultTypes, } @@ -958,7 +1025,7 @@ func TestMatching(t *testing.T) { } // Repeat with a matcher that should now match 3 files. - deployer.cfg.Matchers = []*Matcher{{Pattern: "aaa", Force: true, re: regexp.MustCompile("aaa")}} + deployer.cfg.Matchers = []*deployconfig.Matcher{{Pattern: "aaa", Force: true, Re: regexp.MustCompile("aaa")}} if err := deployer.Deploy(ctx); err != nil { t.Errorf("no-op deploy with triple force matcher: %v", err) } @@ -1026,3 +1093,10 @@ func verifyRemote(ctx context.Context, bucket *blob.Bucket, local []*fileData) ( } return diff, nil } + +func newDeployer() *Deployer { + return &Deployer{ + logger: loggers.NewDefault(), + cfg: deployconfig.DeployConfig{Workers: 2}, + } +} diff --git a/deploy/deployConfig.go b/deploy/deployconfig/deployConfig.go similarity index 79% rename from deploy/deployConfig.go rename to deploy/deployconfig/deployConfig.go index 3f5465171..b16b7c627 100644 --- a/deploy/deployConfig.go +++ b/deploy/deployconfig/deployConfig.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,24 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy - -package deploy +package deployconfig import ( + "errors" "fmt" "regexp" - "errors" - "github.com/gobwas/glob" "github.com/gohugoio/hugo/config" hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/mitchellh/mapstructure" ) -const deploymentConfigKey = "deployment" +const DeploymentConfigKey = "deployment" // DeployConfig is the complete configuration for deployment. type DeployConfig struct { @@ -52,7 +48,7 @@ type DeployConfig struct { // Number of concurrent workers to use when uploading files. Workers int - ordering []*regexp.Regexp // compiled Order + Ordering []*regexp.Regexp `json:"-"` // compiled Order } type Target struct { @@ -71,20 +67,25 @@ type Target struct { Exclude string // Parsed versions of Include/Exclude. - includeGlob glob.Glob - excludeGlob glob.Glob + IncludeGlob glob.Glob `json:"-"` + ExcludeGlob glob.Glob `json:"-"` + + // If true, any local path matching /index.html will be mapped to the + // remote path /. This does not affect the top-level index.html file, + // since that would result in an empty path. + StripIndexHTML bool } -func (tgt *Target) parseIncludeExclude() error { +func (tgt *Target) ParseIncludeExclude() error { var err error if tgt.Include != "" { - tgt.includeGlob, err = hglob.GetGlob(tgt.Include) + tgt.IncludeGlob, err = hglob.GetGlob(tgt.Include) if err != nil { return fmt.Errorf("invalid deployment.target.include %q: %v", tgt.Include, err) } } if tgt.Exclude != "" { - tgt.excludeGlob, err = hglob.GetGlob(tgt.Exclude) + tgt.ExcludeGlob, err = hglob.GetGlob(tgt.Exclude) if err != nil { return fmt.Errorf("invalid deployment.target.exclude %q: %v", tgt.Exclude, err) } @@ -119,24 +120,28 @@ type Matcher struct { // other route-determined metadata (e.g., ContentType) has changed. Force bool - // re is Pattern compiled. - re *regexp.Regexp + // Re is Pattern compiled. + Re *regexp.Regexp `json:"-"` } func (m *Matcher) Matches(path string) bool { - return m.re.MatchString(path) + return m.Re.MatchString(path) +} + +var DefaultConfig = DeployConfig{ + Workers: 10, + InvalidateCDN: true, + MaxDeletes: 256, } // DecodeConfig creates a config from a given Hugo configuration. func DecodeConfig(cfg config.Provider) (DeployConfig, error) { - var ( - dcfg DeployConfig - ) + dcfg := DefaultConfig - if !cfg.IsSet(deploymentConfigKey) { + if !cfg.IsSet(DeploymentConfigKey) { return dcfg, nil } - if err := mapstructure.WeakDecode(cfg.GetStringMap(deploymentConfigKey), &dcfg); err != nil { + if err := mapstructure.WeakDecode(cfg.GetStringMap(DeploymentConfigKey), &dcfg); err != nil { return dcfg, err } @@ -148,7 +153,7 @@ func DecodeConfig(cfg config.Provider) (DeployConfig, error) { if *tgt == (Target{}) { return dcfg, errors.New("empty deployment target") } - if err := tgt.parseIncludeExclude(); err != nil { + if err := tgt.ParseIncludeExclude(); err != nil { return dcfg, err } } @@ -157,7 +162,7 @@ func DecodeConfig(cfg config.Provider) (DeployConfig, error) { if *m == (Matcher{}) { return dcfg, errors.New("empty deployment matcher") } - m.re, err = regexp.Compile(m.Pattern) + m.Re, err = regexp.Compile(m.Pattern) if err != nil { return dcfg, fmt.Errorf("invalid deployment.matchers.pattern: %v", err) } @@ -167,7 +172,7 @@ func DecodeConfig(cfg config.Provider) (DeployConfig, error) { if err != nil { return dcfg, fmt.Errorf("invalid deployment.orderings.pattern: %v", err) } - dcfg.ordering = append(dcfg.ordering, re) + dcfg.Ordering = append(dcfg.Ordering, re) } return dcfg, nil diff --git a/deploy/deployConfig_test.go b/deploy/deployconfig/deployConfig_test.go similarity index 93% rename from deploy/deployConfig_test.go rename to deploy/deployconfig/deployConfig_test.go index 2dbe18715..38d0aadd6 100644 --- a/deploy/deployConfig_test.go +++ b/deploy/deployconfig/deployConfig_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,10 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy +//go:build withdeploy -package deploy +package deployconfig import ( "fmt" @@ -91,7 +90,7 @@ force = true c.Assert(len(dcfg.Order), qt.Equals, 2) c.Assert(dcfg.Order[0], qt.Equals, "o1") c.Assert(dcfg.Order[1], qt.Equals, "o2") - c.Assert(len(dcfg.ordering), qt.Equals, 2) + c.Assert(len(dcfg.Ordering), qt.Equals, 2) // Targets. c.Assert(len(dcfg.Targets), qt.Equals, 3) @@ -104,11 +103,11 @@ force = true c.Assert(tgt.CloudFrontDistributionID, qt.Equals, fmt.Sprintf("cdn%d", i)) c.Assert(tgt.Include, qt.Equals, wantInclude[i]) if wantInclude[i] != "" { - c.Assert(tgt.includeGlob, qt.Not(qt.IsNil)) + c.Assert(tgt.IncludeGlob, qt.Not(qt.IsNil)) } c.Assert(tgt.Exclude, qt.Equals, wantExclude[i]) if wantExclude[i] != "" { - c.Assert(tgt.excludeGlob, qt.Not(qt.IsNil)) + c.Assert(tgt.ExcludeGlob, qt.Not(qt.IsNil)) } } @@ -117,7 +116,7 @@ force = true for i := 0; i < 3; i++ { m := dcfg.Matchers[i] c.Assert(m.Pattern, qt.Equals, fmt.Sprintf("^pattern%d$", i)) - c.Assert(m.re, qt.Not(qt.IsNil)) + c.Assert(m.Re, qt.Not(qt.IsNil)) c.Assert(m.CacheControl, qt.Equals, fmt.Sprintf("cachecontrol%d", i)) c.Assert(m.ContentEncoding, qt.Equals, fmt.Sprintf("contentencoding%d", i)) c.Assert(m.ContentType, qt.Equals, fmt.Sprintf("contenttype%d", i)) diff --git a/deploy/google.go b/deploy/google.go index 6e492bc01..5b302e95b 100644 --- a/deploy/google.go +++ b/deploy/google.go @@ -11,8 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !nodeploy -// +build !nodeploy +//go:build withdeploy package deploy diff --git a/deps/deps.go b/deps/deps.go index 39462de96..d0d6d95fc 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -3,47 +3,50 @@ package deps import ( "context" "fmt" + "io" + "os" "path/filepath" "sort" "strings" "sync" "sync/atomic" + "github.com/bep/logg" + "github.com/gohugoio/hugo/cache/dynacache" + "github.com/gohugoio/hugo/cache/filecache" "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/internal/js" + "github.com/gohugoio/hugo/internal/warpc" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/postpub" + "github.com/gohugoio/hugo/tpl/tplimpl" "github.com/gohugoio/hugo/metrics" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/tpl" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" ) // Deps holds dependencies used by many. // There will be normally only one instance of deps in play // at a given time, i.e. one per Site built. type Deps struct { - // The logger to use. Log loggers.Logger `json:"-"` - // Used to log errors that may repeat itself many times. - LogDistinct loggers.Logger - ExecHelper *hexec.Exec - // The templates to use. This will usually implement the full tpl.TemplateManager. - tmplHandlers *tpl.TemplateHandlers - // The file systems to use. Fs *hugofs.Fs `json:"-"` @@ -62,13 +65,17 @@ type Deps struct { // The configuration to use Conf config.AllProvider `json:"-"` + // The memory cache to use. + MemCache *dynacache.Cache + // The translation func to use Translate func(ctx context.Context, translationID string, templateData any) string `json:"-"` // The site building. Site page.Site - TemplateProvider ResourceProvider + TemplateStore *tplimpl.TemplateStore + // Used in tests OverloadedTemplateFuncs map[string]any @@ -77,14 +84,32 @@ type Deps struct { Metrics metrics.Provider // BuildStartListeners will be notified before a build starts. - BuildStartListeners *Listeners + BuildStartListeners *Listeners[any] + + // BuildEndListeners will be notified after a build finishes. + BuildEndListeners *Listeners[any] + + // OnChangeListeners will be notified when something changes. + OnChangeListeners *Listeners[identity.Identity] // Resources that gets closed when the build is done or the server shuts down. - BuildClosers *Closers + BuildClosers *types.Closers // This is common/global for all sites. BuildState *BuildState + // Misc counters. + Counters *Counters + + // Holds RPC dispatchers for Katex etc. + // TODO(bep) rethink this re. a plugin setup, but this will have to do for now. + WasmDispatchers *warpc.Dispatchers + + // The JS batcher client. + JSBatcherClient js.BatcherClient + + isClosed bool + *globalErrHandler } @@ -99,11 +124,10 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) { } return &d, nil - } -func (d *Deps) SetTempl(t *tpl.TemplateHandlers) { - d.tmplHandlers = t +func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore { + return d.TemplateStore } func (d *Deps) Init() error { @@ -117,27 +141,44 @@ func (d *Deps) Init() error { } if d.Log == nil { - d.Log = loggers.NewErrorLogger() - } - - if d.LogDistinct == nil { - d.LogDistinct = helpers.NewDistinctLogger(d.Log) + d.Log = loggers.NewDefault() } if d.globalErrHandler == nil { - d.globalErrHandler = &globalErrHandler{} + d.globalErrHandler = &globalErrHandler{ + logger: d.Log, + } } - if d.BuildState == nil { d.BuildState = &BuildState{} } + if d.Counters == nil { + d.Counters = &Counters{} + } + if d.BuildState.DeferredExecutions == nil { + if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil { + d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions) + } + d.BuildState.DeferredExecutions = &DeferredExecutions{ + Executions: maps.NewCache[string, *tpl.DeferredExecution](), + FilenamesWithPostPrefix: maps.NewCache[string, bool](), + } + } if d.BuildStartListeners == nil { - d.BuildStartListeners = &Listeners{} + d.BuildStartListeners = &Listeners[any]{} + } + + if d.BuildEndListeners == nil { + d.BuildEndListeners = &Listeners[any]{} } if d.BuildClosers == nil { - d.BuildClosers = &Closers{} + d.BuildClosers = &types.Closers{} + } + + if d.OnChangeListeners == nil { + d.OnChangeListeners = &Listeners[identity.Identity]{} } if d.Metrics == nil && d.Conf.TemplateMetrics() { @@ -145,24 +186,37 @@ func (d *Deps) Init() error { } if d.ExecHelper == nil { - d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config)) + d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config), d.Conf.WorkingDir(), d.Log) + } + + if d.MemCache == nil { + d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log}) } if d.PathSpec == nil { - hashBytesReceiverFunc := func(name string, match bool) { - if !match { - return + hashBytesReceiverFunc := func(name string, match []byte) { + s := string(match) + switch s { + case postpub.PostProcessPrefix: + d.BuildState.AddFilenameWithPostPrefix(name) + case tpl.HugoDeferredTemplatePrefix: + d.BuildState.DeferredExecutions.FilenamesWithPostPrefix.Set(name, true) } - d.BuildState.AddFilenameWithPostPrefix(name) } // Skip binary files. mediaTypes := d.Conf.GetConfigSection("mediaTypes").(media.Types) - hashBytesSHouldCheck := func(name string) bool { + hashBytesShouldCheck := func(name string) bool { ext := strings.TrimPrefix(filepath.Ext(name), ".") return mediaTypes.IsTextSuffix(ext) } - d.Fs.PublishDir = hugofs.NewHasBytesReceiver(d.Fs.PublishDir, hashBytesSHouldCheck, hashBytesReceiverFunc, []byte(postpub.PostProcessPrefix)) + d.Fs.PublishDir = hugofs.NewHasBytesReceiver( + d.Fs.PublishDir, + hashBytesShouldCheck, + hashBytesReceiverFunc, + []byte(tpl.HugoDeferredTemplatePrefix), + []byte(postpub.PostProcessPrefix)) + pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log) if err != nil { return err @@ -189,13 +243,16 @@ func (d *Deps) Init() error { } var common *resources.SpecCommon - var imageCache *resources.ImageCache if d.ResourceSpec != nil { common = d.ResourceSpec.SpecCommon - imageCache = d.ResourceSpec.ImageCache } - resourceSpec, err := resources.NewSpec(d.PathSpec, common, imageCache, d.BuildState, d.Log, d, d.ExecHelper) + fileCaches, err := filecache.NewCaches(d.PathSpec) + if err != nil { + return fmt.Errorf("failed to create file caches from configuration: %w", err) + } + + resourceSpec, err := resources.NewSpec(d.PathSpec, common, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState) if err != nil { return fmt.Errorf("failed to create resource spec: %w", err) } @@ -204,22 +261,17 @@ func (d *Deps) Init() error { return nil } +// TODO(bep) rework this to get it in line with how we manage templates. func (d *Deps) Compile(prototype *Deps) error { var err error if prototype == nil { - if err = d.TemplateProvider.NewResource(d); err != nil { - return err - } + if err = d.TranslationProvider.NewResource(d); err != nil { return err } return nil } - if err = d.TemplateProvider.CloneResource(d, prototype); err != nil { - return err - } - if err = d.TranslationProvider.CloneResource(d, prototype); err != nil { return err } @@ -227,7 +279,26 @@ func (d *Deps) Compile(prototype *Deps) error { return nil } +// MkdirTemp returns a temporary directory path that will be cleaned up on exit. +func (d Deps) MkdirTemp(pattern string) (string, error) { + filename, err := os.MkdirTemp("", pattern) + if err != nil { + return "", err + } + d.BuildClosers.Add( + types.CloserFunc( + func() error { + return os.RemoveAll(filename) + }, + ), + ) + + return filename, nil +} + type globalErrHandler struct { + logger loggers.Logger + // Channel for some "hard to get to" build errors buildErrors chan error // Used to signal that the build is done. @@ -246,8 +317,7 @@ func (e *globalErrHandler) SendError(err error) { } return } - - jww.ERROR.Println(err) + e.logger.Errorln(err) } func (e *globalErrHandler) StartErrorCollector() chan error { @@ -264,15 +334,16 @@ func (e *globalErrHandler) StopErrorCollector() { } // Listeners represents an event listener. -type Listeners struct { +type Listeners[T any] struct { sync.Mutex // A list of funcs to be notified about an event. - listeners []func() + // If the return value is true, the listener will be removed. + listeners []func(...T) bool } // Add adds a function to a Listeners instance. -func (b *Listeners) Add(f func()) { +func (b *Listeners[T]) Add(f func(...T) bool) { if b == nil { return } @@ -282,12 +353,16 @@ func (b *Listeners) Add(f func()) { } // Notify executes all listener functions. -func (b *Listeners) Notify() { +func (b *Listeners[T]) Notify(vs ...T) { b.Lock() defer b.Unlock() + temp := b.listeners[:0] for _, notify := range b.listeners { - notify() + if !notify(vs...) { + temp = append(temp, notify) + } } + b.listeners = temp } // ResourceProvider is used to create and refresh, and clone resources needed. @@ -296,15 +371,18 @@ type ResourceProvider interface { CloneResource(dst, src *Deps) error } -func (d *Deps) Tmpl() tpl.TemplateHandler { - return d.tmplHandlers.Tmpl -} - -func (d *Deps) TextTmpl() tpl.TemplateParseFinder { - return d.tmplHandlers.TxtTmpl -} - func (d *Deps) Close() error { + if d.isClosed { + return nil + } + d.isClosed = true + + if d.MemCache != nil { + d.MemCache.Stop() + } + if d.WasmDispatchers != nil { + d.WasmDispatchers.Close() + } return d.BuildClosers.Close() } @@ -312,9 +390,18 @@ func (d *Deps) Close() error { // on a global level, i.e. logging etc. // Nil values will be given default values. type DepsCfg struct { + // The logger to use. Only set in some tests. + // TODO(bep) get rid of this. + TestLogger loggers.Logger - // The Logger to use. - Logger loggers.Logger + // The logging level to use. + LogLevel logg.Level + + // Logging output. + StdErr io.Writer + + // The console output. + StdOut io.Writer // The file systems to use Fs *hugofs.Fs @@ -329,6 +416,9 @@ type DepsCfg struct { // i18n handling. TranslationProvider ResourceProvider + + // ChangesFromBuild for changes passed back to the server/watch process. + ChangesFromBuild chan []identity.Identity } // BuildState are state used during a build. @@ -337,9 +427,50 @@ type BuildState struct { mu sync.Mutex // protects state below. - // A set of ilenames in /public that + OnSignalRebuild func(ids ...identity.Identity) + + // A set of filenames in /public that // contains a post-processing prefix. filenamesWithPostPrefix map[string]bool + + DeferredExecutions *DeferredExecutions + + // Deferred executions grouped by rendering context. + DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions +} + +// Misc counters. +type Counters struct { + // Counter for the math.Counter function. + MathCounter atomic.Uint64 +} + +type DeferredExecutions struct { + // A set of filenames in /public that + // contains a post-processing prefix. + FilenamesWithPostPrefix *maps.Cache[string, bool] + + // Maps a placeholder to a deferred execution. + Executions *maps.Cache[string, *tpl.DeferredExecution] +} + +var _ identity.SignalRebuilder = (*BuildState)(nil) + +// StartStageRender will be called before a stage is rendered. +func (b *BuildState) StartStageRender(stage tpl.RenderingContext) { +} + +// StopStageRender will be called after a stage is rendered. +func (b *BuildState) StopStageRender(stage tpl.RenderingContext) { + b.DeferredExecutionsGroupedByRenderingContext[stage] = b.DeferredExecutions + b.DeferredExecutions = &DeferredExecutions{ + Executions: maps.NewCache[string, *tpl.DeferredExecution](), + FilenamesWithPostPrefix: maps.NewCache[string, bool](), + } +} + +func (b *BuildState) SignalRebuild(ids ...identity.Identity) { + b.OnSignalRebuild(ids...) } func (b *BuildState) AddFilenameWithPostPrefix(filename string) { @@ -365,30 +496,3 @@ func (b *BuildState) GetFilenamesWithPostPrefix() []string { func (b *BuildState) Incr() int { return int(atomic.AddUint64(&b.counter, uint64(1))) } - -type Closer interface { - Close() error -} - -type Closers struct { - mu sync.Mutex - cs []Closer -} - -func (cs *Closers) Add(c Closer) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.cs = append(cs.cs, c) -} - -func (cs *Closers) Close() error { - cs.mu.Lock() - defer cs.mu.Unlock() - for _, c := range cs.cs { - c.Close() - } - - cs.cs = cs.cs[:0] - - return nil -} diff --git a/docs/.codespellrc b/docs/.codespellrc new file mode 100644 index 000000000..564fc77c0 --- /dev/null +++ b/docs/.codespellrc @@ -0,0 +1,13 @@ +# Config file for codespell. +# https://github.com/codespell-project/codespell#using-a-config-file + +[codespell] + +# Comma separated list of dirs to be skipped. +skip = _vendor,.cspell.json,chroma.css,chroma_dark.css + +# Comma separated list of words to be ignored. Words must be lowercased. +ignore-words-list = abl,edn,te,ue,trys,januar,womens,crossreferences + +# Check file names as well. +check-filenames = true diff --git a/docs/.cspell.json b/docs/.cspell.json index 0958b7645..bf61489da 100644 --- a/docs/.cspell.json +++ b/docs/.cspell.json @@ -1,361 +1,185 @@ { "version": "0.2", - "words": [ - "aaabaab", - "aabb", - "aabba", - "aabbaa", - "aabbaabb", - "aabbaabbab", - "abbaa", - "abourget", - "absurl", - "adoc", - "algolia", - "allowfullscreen", - "ananke", - "anchorize", - "anthonyfok", - "asciidoctor", - "attrlink", - "azblob", - "baseof", - "bbaa", - "bcde", - "bcdef", - "beevelop", - "Bergevin", - "bibtex", - "Bjørn", - "blackfriday", - "blogue", - "bogem", - "Bootcamp", - "brlink", - "Brotli", - "Browsersync", - "canonicalization", - "canonify", - "Catmull", - "Catwoman", - "changefreq", - "Cheatsheet", - "choco", - "chromastyles", - "clockoon", - "Cloudinary", - "CNAME", - "Codecademy's", - "CODEOWNERS", - "Coen", - "Commento", - "Cond", - "contentdir", - "Contentful", - "Copr", - "copyrighthtml", - "corejs", - "countrunes", - "countwords", - "crossreferences", - "daftaupe", - "datatable", - "DATOCMS", - "debugconfig", - "defang", - "Deindent", - "DELIM", - "dhersam", - "digitalcraftsman", - "Disqus", - "Dmdh", - "doas", - "dokuwiki", - "dpkg", - "DRING", - "Eiqc", - "Eliott", - "embeddable", - "Emojify", - "Enwrite", - "eopkg", - "eparis", - "errorf", - "erroridf", - "esbuild", - "Evernote", - "Exif", - "exitwp", - "expirydate", - "Feminella", - "firstpost", - "Flickr", - "Formspree", - "fpath", - "Francia", - "freenode", - "frontmatter", - "funcs", - "funcsig", - "Garen", - "Garuda", - "gcloud", - "Getenv", - "getjson", - "getpage", - "Gitee", - "Gmfc", - "Goel", - "Gohugo", - "gohugoio", - "goldenbridge", - "Goldmark", - "gomodules", - "GOPATH", - "govendor", - "Gowans", - "Grayscale", - "Gregor", - "Gruber", - "gtag", - "gvfs", - "hidecaption", - "hmac", - "Hokus", - "hola", - "hügó", - "hugodeps", - "hugodoc", - "Hugofy", - "hugolang", - "hugoversion", - "Hyas", - "Hyvor", - "iframes", - "ifttt", - "iife", - "imgproc", - "importr", - "IMWQ", - "indice", - "innershortcode", - "Intelli", - "interdoc", - "IPTC", - "ismenucurrent", - "Isset", - "Isso", - "Jaco", - "JIRN", - "johnpatitucci", - "Joomla", - "JRBR", - "jsonify", - "Karmada", - "katex", - "keycdn", - "KEYVALS", - "kubernetes", - "Kubuntu", - "Lanczos", - "langformatnumber", - "lastmod", - "libwebp", - "linktitle", - "Lipi", - "lrwxr", - "Lubuntu", - "maingo", - "markdownified", - "markdownify", - "mathjax", - "mdhender", - "mdshortcode", - "MENUENTRY", - "mercredi", - "Milli", - "Mittwoch", - "mkdir", - "modh", - "monokai", - "Morling", - "mspowerpoint", - "Multihost", - "Muut", - "myclass", - "mydeployment", - "myindex", - "mylayout", - "mylogin", - "mypage", - "mypartials", - "mypost", - "mysite", - "myspa", - "mystyle", - "mytextpartial", - "mytheme", - "NDJSON", - "needsexample", - "Netravali", - "newparam", - "Nichlas", - "Nikhil", - "Nikola", - "Njjy", - "nlist", - "nobr", - "nocopy", - "Norsk", - "nosniff", - "NOSQL", - "notoc", - "novembre", - "numfmt", - "NUMWORKERMULTIPLIER", - "Obhu", - "octohug", - "Octopress", - "oldparam", - "onrender", - "opengraph", - "OWASP", - "Pandoc", - "partialcached", - "Pastorius", - "Patitucci", - "PCRE", - "peaceiris", - "Pedersen", - "Pekka", - "permalinkable", - "plainify", - "POSIX", - "postprocess", - "Poupin", - "prerender", - "println", - "Pritchard", - "publishdate", - "Pygments", - "qref", - "querify", - "QVOMC", - "Racic", - "Rclone", - "rdwatters", - "readfile", - "rebinded", - "recommendedby", - "REDIR", - "reftext", - "relatedfuncs", - "relref", - "relurl", - "remarkjs", - "rgba", - "Riku", - "rlimit", - "roboto", - "rssxml", - "rwxrwxrwx", - "RYUGV", - "safehtml", - "safejs", - "Samsa", - "schemaorg", - "setx", - "Shekhar", - "Shortcode", - "Shortcodes", - "signup", - "Silvola", - "Sindre", - "sitemapindex", - "sitemapxml", - "slugorfilename", - "Smartcrop", - "Sobre", - "Sprintf", - "Startseite", - "strconv", - "stringifier", - "struct", - "structs", - "subdir", - "svgs", - "symdiff", - "Talkyard", - "taxo", - "taxonomyname", - "tbody", - "tdewolff", - "testshortcodes", - "thead", - "Thinkful", - "Tknx", - "TLDR", - "TMPDIR", - "toclevels", - "TOCSS", - "todos", - "tojson", - "Tomango", - "topologix", - "Torikian", - "totoml", - "toyaml", - "twitteruser", - "Unmarshal", - "unpublishdate", - "Unsharp", - "urlize", - "urlset", - "utimestamp", - "vendored", - "vimrc", - "wanghc", - "Wappalyzer", - "warnf", - "webp", - "Wercker", - "wibble", - "wordcount", - "workson", - "Wowchemy", - "wpxr", - "Xbaabbab", - "Xubuntu", - "xvzf", - "yoyoyo", - "yunbox", - "Zgotmpl", - "Zorin", - "zzbbaabb", - "مدونتي" - ], - "language": "en,en-US,de,fr", "allowCompoundWords": true, "files": [ "**/*.md" ], - "ignoreRegExpList": [ - "\\n(`{3,})\\w*\\n[\\s\\S]+?\\1", - "\\[(\\*{2})?@\\w+?\\1\\]", - "\\[`\\w+`\\]", - "ve{2,}r{2,}y", - "ve+r+y+long\\w*", - "\\/.*?\\/", - "\\_\\w+", - "\\#\\w+" + "flagWords": [ + "alot", + "hte", + "langauge", + "reccommend", + "seperate", + "teh" ], "ignorePaths": [ - ".cspell.json", - "**/node_modules/**", - "*.min.*", - "**/news/*", - "**/showcase/*" + "**/emojis.md", + "**/commands/*", + "**/showcase/*", + "**/tools/*" ], - "useGitignore": true, - "enabled": true + "ignoreRegExpList": [ + "# cspell: ignore fenced code blocks", + "^(\\s*`{3,}).*[\\s\\S]*?^\\1$", + "# cspell: ignore words joined with dot", + "\\w+\\.\\w+", + "# cspell: ignore strings within backticks", + "`.+`", + "# cspell: ignore strings within double quotes", + "\".+\"", + "# cspell: ignore strings within brackets", + "\\[.+\\]", + "# cspell: ignore strings within parentheses", + "\\(.+\\)", + "# cspell: ignore words that begin with a slash", + "/\\w+", + "# cspell: ignore everything within action delimiters", + "\\{\\{.+\\}\\}", + "# cspell: ignore everything after a right arrow", + "\\s+→\\s+.+" + ], + "language": "en", + "words": [ + "composability", + "configurators", + "defang", + "deindent", + "downscale", + "downscaling", + "exif", + "geolocalized", + "grayscale", + "marshal", + "marshaling", + "multihost", + "multiplatfom", + "performantly", + "preconfigured", + "prerendering", + "redirection", + "redirections", + "subexpression", + "suppressible", + "synchronisation", + "templating", + "transpile", + "unmarshal", + "unmarshaling", + "unmarshals", + "# ----------------------------------------------------------------------", + "# cspell: ignore hugo terminology", + "# ----------------------------------------------------------------------", + "alignx", + "attrlink", + "canonify", + "codeowners", + "dynacache", + "eturl", + "getenv", + "gohugo", + "gohugoio", + "keyvals", + "leftdelim", + "linkify", + "numworkermultiplier", + "rightdelim", + "shortcode", + "stringifier", + "struct", + "toclevels", + "unmarshal", + "unpublishdate", + "zgotmplz", + "# ----------------------------------------------------------------------", + "# cspell: ignore foreign language words", + "# ----------------------------------------------------------------------", + "bezpieczeństwo", + "blatt", + "buch", + "descripción", + "dokumentation", + "erklärungen", + "libros", + "mercredi", + "miesiąc", + "miesiąc", + "miesiąca", + "miesiące", + "miesięcy", + "misérables", + "mittwoch", + "muchos", + "novembre", + "otro", + "pocos", + "produkte", + "projekt", + "prywatność", + "referenz", + "régime", + "# ----------------------------------------------------------------------", + "# cspell: ignore names", + "# ----------------------------------------------------------------------", + "Atishay", + "Cosette", + "Eliott", + "Furet", + "Gregor", + "Jaco", + "Lanczos", + "Ninke", + "Noll", + "Pastorius", + "Samsa", + "Stucki", + "Thénardier", + "WASI", + "# ----------------------------------------------------------------------", + "# cspell: ignore operating systems and software packages", + "# ----------------------------------------------------------------------", + "asciidoctor", + "brotli", + "cifs", + "corejs", + "disqus", + "docutils", + "dpkg", + "doas", + "eopkg", + "gitee", + "goldmark", + "katex", + "kubuntu", + "lubuntu", + "mathjax", + "nosql", + "pandoc", + "pkgin", + "rclone", + "xubuntu", + "# ----------------------------------------------------------------------", + "# cspell: ignore miscellaneous", + "# ----------------------------------------------------------------------", + "achristie", + "ccpa", + "cpra", + "ddmaurier", + "dring", + "fleqn", + "inor", + "jausten", + "jdoe", + "jsmith", + "leqno", + "milli", + "monokai", + "mysanityprojectid", + "rgba", + "rsmith", + "tdewolff", + "tjones", + "vcard", + "wcag", + "xfeff" + ] } diff --git a/docs/.github/ISSUE_TEMPLATE/config.yml b/docs/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3ba13e0ce --- /dev/null +++ b/docs/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/docs/.github/ISSUE_TEMPLATE/default.md b/docs/.github/ISSUE_TEMPLATE/default.md new file mode 100644 index 000000000..ada35b3a5 --- /dev/null +++ b/docs/.github/ISSUE_TEMPLATE/default.md @@ -0,0 +1,6 @@ +--- +name: Default +about: This is the default issue template. +labels: + - NeedsTriage +--- diff --git a/docs/.github/SUPPORT.md b/docs/.github/SUPPORT.md index cc9de09ff..96a4400c3 100644 --- a/docs/.github/SUPPORT.md +++ b/docs/.github/SUPPORT.md @@ -1,3 +1,3 @@ -### Asking Support Questions +### 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. diff --git a/docs/.github/workflows/codeql-analysis.yml b/docs/.github/workflows/codeql-analysis.yml index 30f98a000..86441b845 100644 --- a/docs/.github/workflows/codeql-analysis.yml +++ b/docs/.github/workflows/codeql-analysis.yml @@ -15,12 +15,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: "javascript" - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/docs/.github/workflows/spellcheck.yml b/docs/.github/workflows/spellcheck.yml index 594621604..e01ab1764 100644 --- a/docs/.github/workflows/spellcheck.yml +++ b/docs/.github/workflows/spellcheck.yml @@ -1,9 +1,9 @@ name: "Check spelling" -on: # rebuild any PRs and main branch changes +on: push: + pull_request: branches-ignore: - "dependabot/**" - pull_request: permissions: contents: read @@ -12,10 +12,16 @@ jobs: spellcheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: streetsidesoftware/cspell-action@v2 + - uses: actions/checkout@v4 + - uses: streetsidesoftware/cspell-action@v5 with: check_dot_files: false + files: content/**/*.md incremental_files_only: true inline: warning strict: false + - uses: codespell-project/actions-codespell@v2 + with: + check_filenames: true + check_hidden: true + # by default, codespell uses configuration from the .codespellrc diff --git a/docs/.github/workflows/super-linter.yml b/docs/.github/workflows/super-linter.yml index efd206960..d8e408ee2 100644 --- a/docs/.github/workflows/super-linter.yml +++ b/docs/.github/workflows/super-linter.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lint Code Base - uses: github/super-linter/slim@v4 + uses: super-linter/super-linter/slim@v6 env: DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/.gitignore b/docs/.gitignore index f9cab2f80..5208c5c3a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,10 +1,12 @@ +.DS_Store +.hugo_build.lock /.idea /.vscode -/public /dist -node_modules +/public +/resources +hugo_stats.json +node_modules/ nohup.out -.DS_Store +package-lock.json trace.out -.hugo_build.lock -resources/_gen/images/ \ No newline at end of file diff --git a/docs/.markdownlint.yaml b/docs/.markdownlint.yaml index 58f84dc67..dbb5b2ee8 100644 --- a/docs/.markdownlint.yaml +++ b/docs/.markdownlint.yaml @@ -1,5 +1,6 @@ # https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md +MD001: false MD002: false MD003: false MD004: false @@ -21,4 +22,6 @@ MD041: false MD046: false MD049: false MD050: false +MD051: false MD053: false +MD055: false diff --git a/docs/.prettierignore b/docs/.prettierignore new file mode 100644 index 000000000..f24bbcef0 --- /dev/null +++ b/docs/.prettierignore @@ -0,0 +1,18 @@ +# Ignore all SVG icons. +**/icons.html + +# These are whitespace sensitive. +layouts/_default/_markup/render-code* +layouts/_default/_markup/render-table* +layouts/shortcodes/glossary-term.html +layouts/shortcodes/glossary.html +layouts/shortcodes/highlighting-styles.html +layouts/shortcodes/list-pages-in-section.html +layouts/shortcodes/quick-reference.html + +# No root node. +layouts/partials/layouts/head/head.html + +# Auto generated. +assets/css/components/chroma*.css +assets/jsconfig.json diff --git a/docs/.prettierrc b/docs/.prettierrc new file mode 100644 index 000000000..395ae39af --- /dev/null +++ b/docs/.prettierrc @@ -0,0 +1,24 @@ +{ + "plugins": [ + "prettier-plugin-go-template", + "@awmottaz/prettier-plugin-void-html" + ], + "overrides": [ + { + "files": ["*.html"], + "options": { + "parser": "go-template", + "goTemplateBracketSpacing": true, + "bracketSameLine": true + } + }, + { + "files": ["*.js", "*.ts"], + "options": { + "useTabs": true, + "printWidth": 120, + "singleQuote": true + } + } + ] +} diff --git a/docs/LICENSE.md b/docs/LICENSE.md index b62a9b5ff..d4facbf8a 100644 --- a/docs/LICENSE.md +++ b/docs/LICENSE.md @@ -1,194 +1,3 @@ -Apache License -============== +See [content/LICENSE.md](content/LICENSE.md) for the license of the content of this repository. -_Version 2.0, January 2004_ -_<>_ - -### Terms and Conditions for use, reproduction, and distribution - -#### 1. Definitions - -“License” shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -“Licensor” shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -“Legal Entity” shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, “control” means **(i)** the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the -outstanding shares, or **(iii)** beneficial ownership of such entity. - -“You” (or “Your”) shall mean an individual or Legal Entity exercising -permissions granted by this License. - -“Source” form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -“Object” form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -“Work” shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -“Derivative Works” shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -“Contribution” shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -“submitted” means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as “Not a Contribution.” - -“Contributor” shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -#### 2. Grant of Copyright License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -#### 3. Grant of Patent License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -#### 4. Redistribution - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -* **(a)** You must give any other recipients of the Work or Derivative Works a copy of -this License; and -* **(b)** You must cause any modified files to carry prominent notices stating that You -changed the files; and -* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. - -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -#### 5. Submission of Contributions - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -#### 6. Trademarks - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -#### 7. Disclaimer of Warranty - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -#### 8. Limitation of Liability - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -#### 9. Accepting Warranty or Additional Liability - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -_END OF TERMS AND CONDITIONS_ - -### APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets `[]` replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same “printed page” as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. +The theme (layouts, CSS, JavaScript etc.) of this repository has no open source license. It is custom made for the Hugo sites and is not meant for reuse. diff --git a/docs/README.md b/docs/README.md index 730ad5fc8..58d0e748c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,57 +1,27 @@ +Hugo + +A fast and flexible static site generator built with love by [bep], [spf13], and [friends] in [Go]. + +--- + [![Netlify Status](https://api.netlify.com/api/v1/badges/e0dbbfc7-34f1-4393-a679-c16e80162705/deploy-status)](https://app.netlify.com/sites/gohugoio/deploys) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://gohugo.io/contribute/documentation/) -# Hugo Docs +This is the repository for the [Hugo](https://github.com/gohugoio/hugo) documentation site. -Documentation site for [Hugo](https://github.com/gohugoio/hugo), the very fast and flexible static site generator built with love in Go. +Please see the [contributing] section for guidelines, examples, and process. -## Contributing +[bep]: https://github.com/bep +[spf13]: https://github.com/spf13 +[friends]: https://github.com/gohugoio/hugo/graphs/contributors +[go]: https://go.dev/ +[contributing]: https://gohugo.io/contribute/documentation -We welcome contributions to Hugo of any kind including documentation, suggestions, bug reports, pull requests etc. Also check out our [contribution guide](https://gohugo.io/contribute/documentation/). We would love to hear from you. +# Install -Note that this repository contains solely the documentation for Hugo. For contributions that aren't documentation-related please refer to the [hugo](https://github.com/gohugoio/hugo) repository. - -*Pull requests shall **only** contain changes to the actual documentation. However, changes on the codebase of Hugo **and** the documentation shall be a single, atomic pull request in the [hugo](https://github.com/gohugoio/hugo) repository.* - -Spelling fixes are most welcomed, and if you want to contribute longer sections to the documentation, it would be great if you had the following criteria in mind when writing: - -* Short is good. People go to the library to read novels. If there is more than one way to _do a thing_ in Hugo, describe the current _best practice_ (avoid "… but you can also do …" and "… in older versions of Hugo you had to …". -* For example, try to find short snippets that teaches people about the concept. If the example is also useful as-is (copy and paste), then great. Don't list long and similar examples just so people can use them on their sites. -* Hugo has users from all over the world, so easy to understand and [simple English](https://simple.wikipedia.org/wiki/Basic_English) is good. - - -## Edit the theme - -If you want to do docs-related theme changes, the simplest way is to have both `hugoDocs` and `gohugoioTheme` cloned as sibling directories, and then run: - -``` -HUGO_MODULE_WORKSPACE=hugo.work hugo server --ignoreVendorPaths "**" +```sh +npm i +hugo server ``` -## Branches - -* The `master` branch is where the site is automatically built from, and is the place to put changes relevant to the current Hugo version. -* The `next` branch is where we store changes that are related to the next Hugo release. This can be previewed here: https://next--gohugoio.netlify.com/ - -## Build - -To view the documentation site locally, you need to clone this repository: - -```bash -git clone https://github.com/gohugoio/hugoDocs.git -``` - -Also note that the documentation version for a given version of Hugo can also be found in the `/docs` sub-folder of the [Hugo source repository](https://github.com/gohugoio/hugo). - -Then to view the docs in your browser, run Hugo and open up the link: - -```bash -▶ hugo server - -Started building sites ... -. -. -Serving pages from memory -Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) -Press Ctrl+C to stop -``` +**Note:** We're working on removing the need to run `npm i` for local development. Stay tuned. diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_algolia.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_algolia.css deleted file mode 100644 index 0122f9758..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_algolia.css +++ /dev/null @@ -1,11 +0,0 @@ -.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;box-sizing:border-box;visibility:visible!important}.searchbox .algolia-autocomplete{display:block;width:100%;height:100%}.searchbox__wrapper{width:100%;height:100%;z-index:999;position:relative}.searchbox__input{display:inline-block;box-sizing:border-box;transition:box-shadow .4s ease,background .4s ease;border:0;border-radius:16px;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0 26px 0 32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none}.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none}.searchbox__input:hover{box-shadow:inset 0 0 0 1px #b3b3b3}.searchbox__input:active,.searchbox__input:focus{outline:0;box-shadow:inset 0 0 0 1px #aaa;background:#fff}.searchbox__input::-webkit-input-placeholder{color:#aaa}.searchbox__input:-ms-input-placeholder{color:#aaa}.searchbox__input::-ms-input-placeholder{color:#aaa}.searchbox__input::placeholder{color:#aaa}.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69,142,225,0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0}.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:""}.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer}.searchbox__submit:focus{outline:0}.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96}.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0,0,0,.5)}.searchbox__reset.hide{display:none}.searchbox__reset:focus{outline:0}.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px}.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:sbx-reset-in;animation-name:sbx-reset-in;-webkit-animation-duration:.15s;animation-duration:.15s}@-webkit-keyframes sbx-reset-in{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}@keyframes sbx-reset-in{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important}.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important}.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px}.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:999;max-width:600px;min-width:500px;box-shadow:0 1px 0 0 rgba(0,0,0,.2),0 2px 3px 0 rgba(0,0,0,.1)}.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:"";width:14px;height:14px;background:#fff;z-index:1000;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:1000;margin-top:8px}.algolia-autocomplete .ds-dropdown-menu .ds-suggestions a:hover{text-decoration:none}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer}.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69,142,225,.05)}.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px}.algolia-autocomplete .ds-dropdown-menu *{box-sizing:border-box}.algolia-autocomplete .algolia-docsearch-suggestion{display:block;position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden}.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143,187,237,.1);padding:.1em .05em}.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;box-shadow:inset 0 -2px 0 0 rgba(69,142,225,.8);color:inherit}.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer}.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:"";position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px}.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d}.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:"";position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0}.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none}.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700}.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d}.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em}.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace}.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none}.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary{display:block}@media (min-width:768px){.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column{display:block}}@media (max-width:768px){.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column{display:inline-block;width:auto;float:left;padding:0;color:#02060c;font-size:.9em;font-weight:700;text-align:left;opacity:.5}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after{content:"|"}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--content{display:inline-block;width:auto;text-align:left;float:left;padding:0}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--content:before{display:none}}.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:"";width:10px;height:10px;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8}.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;box-shadow:none}.algolia-autocomplete .algolia-docsearch-footer{width:134px;height:20px;z-index:2000;margin-top:10.66667px;float:right;font-size:0;line-height:0}.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M78.988.938h16.594a2.968 2.968 0 0 1 2.966 2.966V20.5a2.967 2.967 0 0 1-2.966 2.964H78.988a2.967 2.967 0 0 1-2.966-2.964V3.897A2.961 2.961 0 0 1 78.988.938zm41.937 17.866c-4.386.02-4.386-3.54-4.386-4.106l-.007-13.336 2.675-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-10.846-2.18c.821 0 1.43-.047 1.855-.129v-2.719a6.334 6.334 0 0 0-1.574-.199 5.7 5.7 0 0 0-.897.069 2.699 2.699 0 0 0-.814.24c-.24.116-.439.28-.582.491-.15.212-.219.335-.219.656 0 .628.219.991.616 1.23s.938.362 1.615.362zm-.233-9.7c.883 0 1.629.109 2.231.328.602.218 1.088.525 1.444.915.363.396.609.922.76 1.483.157.56.232 1.175.232 1.85v6.874a32.5 32.5 0 0 1-1.868.314c-.834.123-1.772.185-2.813.185-.69 0-1.327-.069-1.895-.198a4.001 4.001 0 0 1-1.471-.636 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.803 0-.656.13-1.073.384-1.525a3.24 3.24 0 0 1 1.047-1.106c.445-.287.95-.492 1.532-.615a8.8 8.8 0 0 1 1.82-.185 8.404 8.404 0 0 1 1.972.24v-.438c0-.307-.035-.6-.11-.874a1.88 1.88 0 0 0-.384-.73 1.784 1.784 0 0 0-.724-.493 3.164 3.164 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.735 7.735 0 0 0-1.26.307l-.321-2.192c.335-.117.834-.233 1.478-.349a10.98 10.98 0 0 1 2.073-.178zm52.842 9.626c.822 0 1.43-.048 1.854-.13V13.7a6.347 6.347 0 0 0-1.574-.199c-.294 0-.595.021-.896.069a2.7 2.7 0 0 0-.814.24 1.46 1.46 0 0 0-.582.491c-.15.212-.218.335-.218.656 0 .628.218.991.615 1.23.404.245.938.362 1.615.362zm-.226-9.694c.883 0 1.629.108 2.231.327.602.219 1.088.526 1.444.915.355.39.609.923.759 1.483a6.8 6.8 0 0 1 .233 1.852v6.873c-.41.088-1.034.19-1.868.314-.834.123-1.772.184-2.813.184-.69 0-1.327-.068-1.895-.198a4.001 4.001 0 0 1-1.471-.635 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.804 0-.656.13-1.073.384-1.524.26-.45.608-.82 1.047-1.107.445-.286.95-.491 1.532-.614a8.803 8.803 0 0 1 2.751-.13c.329.034.671.096 1.04.185v-.437a3.3 3.3 0 0 0-.109-.875 1.873 1.873 0 0 0-.384-.731 1.784 1.784 0 0 0-.724-.492 3.165 3.165 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.75 7.75 0 0 0-1.26.307l-.321-2.193c.335-.116.834-.232 1.478-.348a11.633 11.633 0 0 1 2.073-.177zm-8.034-1.271a1.626 1.626 0 0 1-1.628-1.62c0-.895.725-1.62 1.628-1.62.904 0 1.63.725 1.63 1.62 0 .895-.733 1.62-1.63 1.62zm1.348 13.22h-2.689V7.27l2.69-.423v11.956zm-4.714 0c-4.386.02-4.386-3.54-4.386-4.107l-.008-13.336 2.676-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-8.698-5.903c0-1.156-.253-2.119-.746-2.788-.493-.677-1.183-1.01-2.067-1.01-.882 0-1.574.333-2.065 1.01-.493.676-.733 1.632-.733 2.788 0 1.168.246 1.953.74 2.63.492.683 1.183 1.018 2.066 1.018.882 0 1.574-.342 2.067-1.019.492-.683.738-1.46.738-2.63zm2.737-.007c0 .902-.13 1.584-.397 2.33a5.52 5.52 0 0 1-1.128 1.906 4.986 4.986 0 0 1-1.752 1.223c-.685.286-1.739.45-2.265.45-.528-.006-1.574-.157-2.252-.45a5.096 5.096 0 0 1-1.744-1.223c-.487-.527-.863-1.162-1.137-1.906a6.345 6.345 0 0 1-.41-2.33c0-.902.123-1.77.397-2.508a5.554 5.554 0 0 1 1.15-1.892 5.133 5.133 0 0 1 1.75-1.216c.679-.287 1.425-.423 2.232-.423.808 0 1.553.142 2.237.423a4.88 4.88 0 0 1 1.753 1.216 5.644 5.644 0 0 1 1.135 1.892c.287.738.431 1.606.431 2.508zm-20.138 0c0 1.12.246 2.363.738 2.882.493.52 1.13.78 1.91.78.424 0 .828-.062 1.204-.178.377-.116.677-.253.917-.417V9.33a10.476 10.476 0 0 0-1.766-.226c-.971-.028-1.71.37-2.23 1.004-.513.636-.773 1.75-.773 2.788zm7.438 5.274c0 1.824-.466 3.156-1.404 4.004-.936.846-2.367 1.27-4.296 1.27-.705 0-2.17-.137-3.34-.396l.431-2.118c.98.205 2.272.26 2.95.26 1.074 0 1.84-.219 2.299-.656.459-.437.684-1.086.684-1.948v-.437a8.07 8.07 0 0 1-1.047.397c-.43.13-.93.198-1.492.198-.739 0-1.41-.116-2.018-.349a4.206 4.206 0 0 1-1.567-1.025c-.431-.45-.774-1.017-1.013-1.694-.24-.677-.363-1.885-.363-2.773 0-.834.13-1.88.384-2.577.26-.696.629-1.298 1.129-1.796.493-.498 1.095-.881 1.8-1.162a6.605 6.605 0 0 1 2.428-.457c.87 0 1.67.109 2.45.24.78.129 1.444.265 1.985.415V18.17z' fill='%235468FF'/%3E%3Cpath d='M6.972 6.677v1.627c-.712-.446-1.52-.67-2.425-.67-.585 0-1.045.13-1.38.391a1.24 1.24 0 0 0-.502 1.03c0 .425.164.765.494 1.02.33.256.835.532 1.516.83.447.192.795.356 1.045.495.25.138.537.332.862.582.324.25.563.548.718.894.154.345.23.741.23 1.188 0 .947-.334 1.691-1.004 2.234-.67.542-1.537.814-2.601.814-1.18 0-2.16-.229-2.936-.686v-1.708c.84.628 1.814.942 2.92.942.585 0 1.048-.136 1.388-.407.34-.271.51-.646.51-1.125 0-.287-.1-.55-.302-.79-.203-.24-.42-.42-.655-.542-.234-.123-.585-.29-1.053-.503a61.27 61.27 0 0 1-.582-.271 13.67 13.67 0 0 1-.55-.287 4.275 4.275 0 0 1-.567-.351 6.92 6.92 0 0 1-.455-.4c-.18-.17-.31-.34-.39-.51-.08-.17-.155-.37-.224-.598a2.553 2.553 0 0 1-.104-.742c0-.915.333-1.638.998-2.17.664-.532 1.523-.798 2.576-.798.968 0 1.793.17 2.473.51zm7.468 5.696v-.287c-.022-.607-.187-1.088-.495-1.444-.309-.357-.75-.535-1.324-.535-.532 0-.99.194-1.373.583-.382.388-.622.949-.717 1.683h3.909zm1.005 2.792v1.404c-.596.34-1.383.51-2.362.51-1.255 0-2.255-.377-3-1.132-.744-.755-1.116-1.744-1.116-2.968 0-1.297.34-2.316 1.021-3.055.68-.74 1.548-1.11 2.6-1.11 1.033 0 1.852.323 2.458.966.606.644.91 1.572.91 2.784 0 .33-.033.676-.096 1.038h-5.314c.107.702.405 1.239.894 1.611.49.372 1.106.558 1.85.558.862 0 1.58-.202 2.155-.606zm6.605-1.77h-1.212c-.596 0-1.045.116-1.349.35-.303.234-.454.532-.454.894 0 .372.117.664.35.877.235.213.575.32 1.022.32.51 0 .912-.142 1.204-.424.293-.281.44-.651.44-1.108v-.91zm-4.068-2.554V9.325c.627-.361 1.457-.542 2.489-.542 2.116 0 3.175 1.026 3.175 3.08V17h-1.548v-.957c-.415.68-1.143 1.02-2.186 1.02-.766 0-1.38-.22-1.843-.661-.462-.442-.694-1.003-.694-1.684 0-.776.293-1.38.878-1.81.585-.431 1.404-.647 2.457-.647h1.34V11.8c0-.554-.133-.971-.399-1.253-.266-.282-.707-.423-1.324-.423a4.07 4.07 0 0 0-2.345.718zm9.333-1.93v1.42c.394-1 1.101-1.5 2.123-1.5.148 0 .313.016.494.048v1.531a1.885 1.885 0 0 0-.75-.143c-.542 0-.989.24-1.34.718-.351.479-.527 1.048-.527 1.707V17h-1.563V8.91h1.563zm5.01 4.084c.022.82.272 1.492.75 2.019.479.526 1.15.79 2.01.79.639 0 1.235-.176 1.788-.527v1.404c-.521.319-1.186.479-1.995.479-1.265 0-2.276-.4-3.031-1.197-.755-.798-1.133-1.792-1.133-2.984 0-1.16.38-2.151 1.14-2.975.761-.825 1.79-1.237 3.088-1.237.702 0 1.346.149 1.93.447v1.436a3.242 3.242 0 0 0-1.77-.495c-.84 0-1.513.266-2.019.798-.505.532-.758 1.213-.758 2.042zM40.24 5.72v4.579c.458-1 1.293-1.5 2.505-1.5.787 0 1.42.245 1.899.734.479.49.718 1.17.718 2.042V17h-1.564v-5.106c0-.553-.14-.98-.422-1.284-.282-.303-.652-.455-1.11-.455-.531 0-1.002.202-1.411.606-.41.405-.615 1.022-.615 1.851V17h-1.563V5.72h1.563zm14.966 10.02c.596 0 1.096-.253 1.5-.758.404-.506.606-1.157.606-1.955 0-.915-.202-1.62-.606-2.114-.404-.495-.92-.742-1.548-.742-.553 0-1.05.224-1.491.67-.442.447-.662 1.133-.662 2.058 0 .958.212 1.67.638 2.138.425.469.946.703 1.563.703zM53.004 5.72v4.42c.574-.894 1.388-1.341 2.44-1.341 1.022 0 1.857.383 2.506 1.149.649.766.973 1.781.973 3.047 0 1.138-.309 2.109-.925 2.912-.617.803-1.463 1.205-2.537 1.205-1.075 0-1.894-.447-2.457-1.34V17h-1.58V5.72h1.58zm9.908 11.104l-3.223-7.913h1.739l1.005 2.632 1.26 3.415c.096-.32.48-1.458 1.15-3.415l.909-2.632h1.66l-2.92 7.866c-.777 2.074-1.963 3.11-3.559 3.11a2.92 2.92 0 0 1-.734-.079v-1.34c.17.042.351.064.543.064 1.032 0 1.755-.57 2.17-1.708z' fill='%235D6494'/%3E%3Cpath d='M89.632 5.967v-.772a.978.978 0 0 0-.978-.977h-2.28a.978.978 0 0 0-.978.977v.793c0 .088.082.15.171.13a7.127 7.127 0 0 1 1.984-.28c.65 0 1.295.088 1.917.259.082.02.164-.04.164-.13m-6.248 1.01l-.39-.389a.977.977 0 0 0-1.382 0l-.465.465a.973.973 0 0 0 0 1.38l.383.383c.062.061.15.047.205-.014.226-.307.472-.601.746-.874.281-.28.568-.526.883-.751.068-.042.075-.137.02-.2m4.16 2.453v3.341c0 .096.104.165.192.117l2.97-1.537c.068-.034.089-.117.055-.184a3.695 3.695 0 0 0-3.08-1.866c-.068 0-.136.054-.136.13m0 8.048a4.489 4.489 0 0 1-4.49-4.482 4.488 4.488 0 0 1 4.49-4.482 4.488 4.488 0 0 1 4.489 4.482 4.484 4.484 0 0 1-4.49 4.482m0-10.85a6.363 6.363 0 1 0 0 12.729 6.37 6.37 0 0 0 6.372-6.368 6.358 6.358 0 0 0-6.371-6.36' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block} -/*# sourceMappingURL=docsearch.min.css.map */ -a.algolia-docsearch-suggestion { - text-decoration: none !important; -} -.algolia-docsearch-suggestion--category-header { - background: #0594cb; - padding-left: .25rem !important; - color: white !important; - border-radius: 3px; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_animation.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_animation.css deleted file mode 100644 index 997931ac4..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_animation.css +++ /dev/null @@ -1,21 +0,0 @@ -.animated { - animation-duration: .5s; - animation-fill-mode: forwards; - animation-timing-function: ease-in-out; -} - -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} -.fadeIn { - animation-name: fadeIn; -} -.animated-delay-1 { - animation-delay: 0.5s; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_carousel.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_carousel.css deleted file mode 100644 index 11fae8702..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_carousel.css +++ /dev/null @@ -1,25 +0,0 @@ -/* These styles enhance the home page carousel, located here: themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html */ -.overflow-x-scroll{ - -webkit-overflow-scrolling: touch; -} -.row { - transition: 450ms transform; - font-size: 0; -} -.tile { - transition: 450ms all; -} -.details { - background: -webkit-gradient(linear, left bottom, left top, from(rgba(0,0,0,0.9)), to(rgba(0,0,0,0))); - background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%); - transition: 450ms opacity; -} -.tile:hover .details { - opacity: 1; -} -.row:hover .tile { - opacity: 0.3; -} -.row:hover .tile:hover { - opacity: 1; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_chroma.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_chroma.css deleted file mode 100644 index d00ea65e6..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_chroma.css +++ /dev/null @@ -1,65 +0,0 @@ -/* Background */ .chroma { background-color: #ffffff } -/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } -/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } -/* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } -/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } -/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } -/* Keyword */ .chroma .k { font-weight: bold } -/* KeywordConstant */ .chroma .kc { font-weight: bold } -/* KeywordDeclaration */ .chroma .kd { font-weight: bold } -/* KeywordNamespace */ .chroma .kn { font-weight: bold } -/* KeywordPseudo */ .chroma .kp { font-weight: bold } -/* KeywordReserved */ .chroma .kr { font-weight: bold } -/* KeywordType */ .chroma .kt { color: #445588; font-weight: bold } -/* NameAttribute */ .chroma .na { color: #008080 } -/* NameBuiltin */ .chroma .nb { color: #999999 } -/* NameClass */ .chroma .nc { color: #445588; font-weight: bold } -/* NameConstant */ .chroma .no { color: #008080 } -/* NameEntity */ .chroma .ni { color: #800080 } -/* NameException */ .chroma .ne { color: #990000; font-weight: bold } -/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold } -/* NameNamespace */ .chroma .nn { color: #555555 } -/* NameTag */ .chroma .nt { color: #000080 } -/* NameVariable */ .chroma .nv { color: #008080 } -/* LiteralString */ .chroma .s { color: #bb8844 } -/* LiteralStringAffix */ .chroma .sa { color: #bb8844 } -/* LiteralStringBacktick */ .chroma .sb { color: #bb8844 } -/* LiteralStringChar */ .chroma .sc { color: #bb8844 } -/* LiteralStringDelimiter */ .chroma .dl { color: #bb8844 } -/* LiteralStringDoc */ .chroma .sd { color: #bb8844 } -/* LiteralStringDouble */ .chroma .s2 { color: #bb8844 } -/* LiteralStringEscape */ .chroma .se { color: #bb8844 } -/* LiteralStringHeredoc */ .chroma .sh { color: #bb8844 } -/* LiteralStringInterpol */ .chroma .si { color: #bb8844 } -/* LiteralStringOther */ .chroma .sx { color: #bb8844 } -/* LiteralStringRegex */ .chroma .sr { color: #808000 } -/* LiteralStringSingle */ .chroma .s1 { color: #bb8844 } -/* LiteralStringSymbol */ .chroma .ss { color: #bb8844 } -/* LiteralNumber */ .chroma .m { color: #009999 } -/* LiteralNumberBin */ .chroma .mb { color: #009999 } -/* LiteralNumberFloat */ .chroma .mf { color: #009999 } -/* LiteralNumberHex */ .chroma .mh { color: #009999 } -/* LiteralNumberInteger */ .chroma .mi { color: #009999 } -/* LiteralNumberIntegerLong */ .chroma .il { color: #009999 } -/* LiteralNumberOct */ .chroma .mo { color: #009999 } -/* Operator */ .chroma .o { font-weight: bold } -/* OperatorWord */ .chroma .ow { font-weight: bold } -/* Comment */ .chroma .c { color: #999988; font-style: italic } -/* CommentHashbang */ .chroma .ch { color: #999988; font-style: italic } -/* CommentMultiline */ .chroma .cm { color: #999988; font-style: italic } -/* CommentSingle */ .chroma .c1 { color: #999988; font-style: italic } -/* CommentSpecial */ .chroma .cs { color: #999999; font-weight: bold; font-style: italic } -/* CommentPreproc */ .chroma .cp { color: #999999; font-weight: bold } -/* CommentPreprocFile */ .chroma .cpf { color: #999999; font-weight: bold } -/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd } -/* GenericEmph */ .chroma .ge { font-style: italic } -/* GenericError */ .chroma .gr { color: #aa0000 } -/* GenericHeading */ .chroma .gh { color: #999999 } -/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd } -/* GenericOutput */ .chroma .go { color: #888888 } -/* GenericPrompt */ .chroma .gp { color: #555555 } -/* GenericStrong */ .chroma .gs { font-weight: bold } -/* GenericSubheading */ .chroma .gu { color: #aaaaaa } -/* GenericTraceback */ .chroma .gt { color: #aa0000 } -/* TextWhitespace */ .chroma .w { color: #bbbbbb } diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_code.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_code.css deleted file mode 100644 index c82e77ee7..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_code.css +++ /dev/null @@ -1,89 +0,0 @@ -.chroma .lntable pre { - padding: 0; - margin: 0; - border: 0; -} - -.chroma .lntable pre code { - padding: 0; - margin: 0; -} - -code { - padding: 0.2em; - margin: 0; - font-size: 85%; - background-color: rgba(27,31,35,0.05); - border-radius: 3px; -} - -pre code { - display: block; - padding: 1.5em 1.5em; - font-size: .875rem; - line-height: 2; - overflow-x: auto; -} - -pre { - background-color: #fff; - color: #333; - white-space: pre; - hyphens: none; - position: relative; - border-width: 1px; - border-color: #ccc; - border-style: solid; -} - -/* The Pygments highlighter comes with its own styles. */ -.highlight pre { - background-color: inherit; - color: inherit; - padding: 0.5em; - font-size: .875rem; -} - - -/*We are adding the copy button content here so we can change it with javascript. See the "Clipboard scripts"*/ -.copy:after { - content: "Copy" -} -.copied:after { - content: "Copied" -} - -@media (--breakpoint-large) { - .full-width - { - /*width: 100vw; - position: relative; - left: 50%; - right: 50%; - margin-left: -50vw; - margin-right: -50vw;*/ - /*width: 60vw;*/ - /*position: relative; - left: 50%; - right: 50%;*/ - /*margin-left: -30vw;*/ - margin-right: -30vw; - max-width: 100vw; - } -} - -.code-block .line-numbers-rows { - background: #2f3a46; - border: none; - bottom: -50px; - color: #98a4b3; - left: -178px; - padding: 50px 0; - top: -50px; - width: 138px -} - -.code-block .line-numbers-rows>span:before { - color: inherit; - padding-right: 30px -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_color-scheme.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_color-scheme.css deleted file mode 100644 index 1d61a7725..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_color-scheme.css +++ /dev/null @@ -1,38 +0,0 @@ -.primary-color {color: var(--primary-color)} -.bg-primary-color {background-color: var(--primary-color)} -.hover-bg-primary-color:hover {background-color: var(--primary-color)} - -.primary-color-dark {color: var(--primary-color-dark)} -.bg-primary-color-dark {background-color: var(--primary-color-dark)} -.hover-bg-primary-color-dark:hover {background-color: var(--primary-color-dark)} - -.primary-color-light {color: var(--primary-color-light)} -.bg-primary-color-light {background-color: var(--primary-color-light)} -.hover-bg-primary-color-light:hover {background-color: var(--primary-color-light)} - -.accent-color {color: var(--accent-color)} -.bg-accent-color {background-color: var(--accent-color)} -.hover-bg-accent-color:hover {background-color: var(--accent-color)} - -.accent-color-light {color: var(--accent-color-light)} -.hover-accent-color-light:hover {color: var(--accent-color-light)} -.bg-accent-color-light {background-color: var(--accent-color-light)} -.hover-bg-accent-color-light:hover {background-color: var(--accent-color-light)} - -.accent-color-dark {color: var(--accent-color-dark)} -.bg-accent-color-dark {background-color: var(--accent-color-dark)} -.hover-bg-accent-color-dark:hover {background-color: var(--accent-color-dark)} - -.text-color-primary {color: var(--text-color-primary)} -.text-on-primary-color {color: var(--text-on-primary-color)} -.text-color-secondary {color: var(--text-color-secondary)} -.text-color-disabled {color: var(--text-color-disabled)} -.divider-color {color: var(--divider-color)} -.warn-color {color: var(--warn-color)} - - -.nested-links a { - color: var(--primary-color); - text-decoration: none; - -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_columns.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_columns.css deleted file mode 100644 index e1e938c74..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_columns.css +++ /dev/null @@ -1,11 +0,0 @@ -.column-count-2 {column-count: 1} -.column-gap-1 {column-gap: 0} -.break-inside-avoid {break-inside: auto} - - -@media (--breakpoint-large) { - .column-count-3-l {column-count: 3} - .column-count-2-l {column-count: 2} - .column-gap-1-l {column-gap: 1} - .break-inside-avoid-l {break-inside: avoid} -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content-tables.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content-tables.css deleted file mode 100644 index 4e092e8bf..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content-tables.css +++ /dev/null @@ -1,28 +0,0 @@ -.prose table { - width: 100%; - margin-bottom: 3em; - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; - border: 1px solid var(--light-gray); - & th { - background-color: var(--primary-color); - border-bottom: 1px solid var(--primary-color); - color: white; - font-weight: 400; - - text-align: left; - padding: .375em .5em; - } - - & td, & tc { - padding: .75em .5em; - text-align: left; - border-right: 1px solid var(--light-gray); - } - -} - -.prose table tr:nth-child(even) { - background-color: var(--light-gray); -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content.css deleted file mode 100644 index 9c8a8a14d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_content.css +++ /dev/null @@ -1,41 +0,0 @@ -.prose ul, .prose ol { - margin-bottom: 2em; -} -.prose ul li, .prose ol li { - margin-bottom: .5em; -} -.prose li:hover { - background-color: var(--light-gray) -} -.prose ::selection { - background: var(--primary-color); /* WebKit/Blink Browsers */ - color: white; -} - - -body { - -line-height: 1.45; - -} - -p {margin-bottom: 1.3em;} - -h1, h2, h3, h4 { -margin: 1.414em 0 0.5em; - -line-height: 1.2; -} - -h1 { -margin-top: 0; -font-size: 2.441em; -} - -h2 {font-size: 1.953em;} - -h3 {font-size: 1.563em;} - -h4 {font-size: 1.25em;} - -small, .font_small {font-size: 0.8em;} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_definition-lists.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_definition-lists.css deleted file mode 100644 index e28f67d4b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_definition-lists.css +++ /dev/null @@ -1,9 +0,0 @@ - -dl dt { - font-weight: bold; - font-size: 1.125rem; -} -dd { - margin: .5em 0 2em 0; - padding: 0; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_documentation-styles.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_documentation-styles.css deleted file mode 100644 index 0ea8e9b72..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_documentation-styles.css +++ /dev/null @@ -1,54 +0,0 @@ -.note, -.warning { - - border-left-width: 4px; - border-left-style: solid; - position: relative; - border-color: var(--primary-color); - - display: block; -} -.note #exclamation-icon, -.warning #exclamation-icon { - - fill: var(--primary-color); - position: absolute; - top: 35%; - left: -12px; - /*background-color: white;*/ -} - - .admonition-content { - display: block; - margin: 0px; - padding: .125em 1em; - /*margin-left: 1em;*/ - margin-top: 2em; - margin-bottom: 2em; - overflow-x: auto; - /*font-size: .9375em;*/ - background-color: var(--black-05); - } - - - .hide-child-menu .child-menu { - display: none; - } - .hide-child-menu:hover .child-menu, - .hide-child-menu:focus .child-menu, - .hide-child-menu:active .child-menu { - display: block; - } - - -/*documentation-copy headings exaggerate spacing and size to chunk content */ - .documentation-copy h2 { - margin-top: 3em; - &.minor { - font-size: inherit; - margin-top: inherit; - border-bottom: none; - } - } - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_fluid-type.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_fluid-type.css deleted file mode 100644 index da9f04c81..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_fluid-type.css +++ /dev/null @@ -1,10 +0,0 @@ -.f2-fluid { - font-size: 2.25rem; -} - -@media (--breakpoint-large) { - .f2-fluid { - font-size: 1.25rem; - font-size: calc(0.875rem + 0.5 * ((100vw - 20rem) / 60)); - } -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_font-family.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_font-family.css deleted file mode 100644 index 440b5efdd..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_font-family.css +++ /dev/null @@ -1,80 +0,0 @@ -/* From https://www.cssfontstack.com */ -code, .code, pre code, .highlight pre { - font-family: 'inconsolata',Menlo,Monaco,'Courier New',monospace; -} - -.sans-serif { - font-family: 'Muli', - avenir, - 'helvetica neue', helvetica, - ubuntu, - roboto, noto, - 'segoe ui', arial, - sans-serif; -} - - -.serif { - font-family: Palatino,"Palatino Linotype","Palatino LT STD","Book Antiqua",Georgia,serif; -} - -/* Monospaced Typefaces (for code) */ - - -.courier { - font-family: 'Courier Next', - courier, - monospace; -} - - -/* Sans-Serif Typefaces */ - -.helvetica { - font-family: 'helvetica neue', helvetica, - sans-serif; -} - -.avenir { - font-family: 'avenir next', avenir, - sans-serif; -} - - -/* Serif Typefaces */ - -.athelas { - font-family: athelas, - georgia, - serif; -} - -.georgia { - font-family: georgia, - serif; -} - -.times { - font-family: times, - serif; -} - -.bodoni { - font-family: "Bodoni MT", - serif; -} - -.calisto { - font-family: "Calisto MT", - serif; -} - -.garamond { - font-family: garamond, - serif; -} - -.baskerville { - font-family: baskerville, - serif; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_header-link.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_header-link.css deleted file mode 100644 index 56a16be6d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_header-link.css +++ /dev/null @@ -1,15 +0,0 @@ -.header-link:after { - position: relative; - left: 0.5em; - opacity: 0; - font-size: 0.8em; - -moz-transition: opacity 0.2s ease-in-out 0.1s; - -ms-transition: opacity 0.2s ease-in-out 0.1s; -} -h2:hover .header-link, -h3:hover .header-link, -h4:hover .header-link, -h5:hover .header-link, -h6:hover .header-link { - opacity: 1; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_hugo-internal-template-styling.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_hugo-internal-template-styling.css deleted file mode 100644 index 0b1df9610..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_hugo-internal-template-styling.css +++ /dev/null @@ -1,52 +0,0 @@ -/* pagination.html: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/template_embedded.go#L117 */ -.pagination { - margin: 3rem 0; -} - -.pagination li { - display: inline-block; - margin-right: .375rem; - font-size: .875rem; - margin-bottom: 2.5em; -} -.pagination li a { - padding: .5rem .625rem; - background-color: white; - color: #333; - border: 1px solid #ddd; - border-radius: 3px; - text-decoration: none; -} -.pagination li.disabled { - display: none; -} -.pagination li.active a:link, -.pagination li.active a:active, -.pagination li.active a:visited { - background-color: #ddd; -} - -/* Hides non-meaningful TOC items*/ -#TableOfContents ul li ul li ul li{ - display: none; - } - - -#TableOfContents ul li { - color: black; - display: block; - margin-bottom: .375em; - line-height: 1.375; -} - -#TableOfContents ul li a{ - width: 100%; - padding: .25em .375em; - margin-left: -.375em; - -} -#TableOfContents ul li a:hover { - background-color: #999; - color: white; - -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_no-js.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_no-js.css deleted file mode 100644 index 7991450fe..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_no-js.css +++ /dev/null @@ -1,7 +0,0 @@ -.no-js .needs-js { - opacity: 0 -} -.js .needs-js { - opacity: 1; - transition: opacity .15s ease-in; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_social-icons.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_social-icons.css deleted file mode 100644 index 04ea11ec5..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_social-icons.css +++ /dev/null @@ -1,23 +0,0 @@ -.facebook, .twitter, .instagram, .youtube { - fill: #BABABA; -} -.facebook:hover { - fill: #3b5998; -} - -.twitter { - fill: #55acee; -} - -.twitter:hover { - fill: #BABABA; -} - - -.instagram:hover { - fill: #e95950; -} - -.youtube:hover { - fill: #bb0000; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_stickyheader.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_stickyheader.css deleted file mode 100644 index 7759bed96..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_stickyheader.css +++ /dev/null @@ -1,15 +0,0 @@ - -@media (min-width: 75em) { - - [data-scrolldir="down"] .sticky { - position: fixed; - top:100px; - right:0; - } - - [data-scrolldir="up"] .sticky { - position: fixed; - top:100px; - right:0; - } -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_svg.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_svg.css deleted file mode 100644 index 299a4a963..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_svg.css +++ /dev/null @@ -1 +0,0 @@ -.fill-current { fill: currentColor; } diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tabs.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tabs.css deleted file mode 100644 index 6e0022cc9..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tabs.css +++ /dev/null @@ -1,34 +0,0 @@ -.tab-button{ - margin-bottom:1px; - position: relative; - z-index: 1; - color:#333; - border-color:#ccc; - outline: none; - background-color:white; -} -.tab-pane code{ - background:#f1f2f2; - border-radius:0; -} -.tab-pane .chroma{ - background:none; - padding:0; -} -.tab-button.active{ - border-bottom-color:#f1f2f2; - background-color: #f1f2f2; -} -.tab-content .tab-pane{ - display: none; -} -.tab-content .tab-pane.active{ - display: block; -} -/* Treatment of copy buttons inside a tab module */ -.tab-content .copy, .tab-content .copied{ - display: none; -} -.tab-content .tab-pane.active + .copy, .tab-content .tab-pane.active + .copied{ - display: block; -} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tachyons.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tachyons.css deleted file mode 100644 index d697c4d85..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_tachyons.css +++ /dev/null @@ -1,94 +0,0 @@ -/*! TACHYONS v4.7.0 | http://tachyons.io */ - -/* - * NOTE: The Tachyons folder is for backup/reference only. This file references the module - * ________ ______ - * ___ __/_____ _________ /______ ______________________ - * __ / _ __ `/ ___/_ __ \_ / / / __ \_ __ \_ ___/ - * _ / / /_/ // /__ _ / / / /_/ // /_/ / / / /(__ ) - * /_/ \__,_/ \___/ /_/ /_/_\__, / \____//_/ /_//____/ - * /____/ - * - * TABLE OF CONTENTS - * - * 1. External Library Includes - * - Normalize.css | http://normalize.css.github.io - * 2. Tachyons Modules - * 3. Variables - * - Media Queries - * - Colors - * 4. Debugging - * - Debug all - * - Debug children - * - */ - - -/* External Library Includes */ -@import 'tachyons/src/_normalize'; - - -/* Modules */ -@import 'tachyons/src/_box-sizing'; -/*@import 'tachyons/src/_aspect-ratios';*/ -@import 'tachyons/src/_images'; -@import 'tachyons/src/_background-size'; -@import 'tachyons/src/_background-position'; -/*@import 'tachyons/src/_outlines';*/ -@import 'tachyons/src/_borders'; -@import 'tachyons/src/_border-colors'; -@import 'tachyons/src/_border-radius'; -@import 'tachyons/src/_border-style'; -@import 'tachyons/src/_border-widths'; -@import 'tachyons/src/_box-shadow'; -/*@import 'tachyons/src/_code';*/ -@import 'tachyons/src/_coordinates'; -@import 'tachyons/src/_clears'; -@import 'tachyons/src/_display'; -@import 'tachyons/src/_flexbox'; -@import 'tachyons/src/_floats'; -/*@import 'tachyons/src/_font-family';*/ -@import 'tachyons/src/_font-style'; -@import 'tachyons/src/_font-weight'; -@import 'tachyons/src/_forms'; -@import 'tachyons/src/_heights'; -@import 'tachyons/src/_letter-spacing'; -@import 'tachyons/src/_line-height'; -@import 'tachyons/src/_links'; -@import 'tachyons/src/_lists'; -@import 'tachyons/src/_max-widths'; -@import 'tachyons/src/_widths'; -@import 'tachyons/src/_overflow'; -@import 'tachyons/src/_position'; -@import 'tachyons/src/_opacity'; -/*@import 'tachyons/src/_rotations';*/ -@import 'tachyons/src/_skins'; -@import 'tachyons/src/_skins-pseudo'; -@import 'tachyons/src/_spacing'; -@import 'tachyons/src/_negative-margins'; -@import 'tachyons/src/_tables'; -@import 'tachyons/src/_text-decoration'; -@import 'tachyons/src/_text-align'; -@import 'tachyons/src/_text-transform'; -@import 'tachyons/src/_type-scale'; -@import 'tachyons/src/_typography'; -@import 'tachyons/src/_utilities'; -@import 'tachyons/src/_visibility'; -@import 'tachyons/src/_white-space'; -@import 'tachyons/src/_vertical-align'; -@import 'tachyons/src/_hovers'; -@import 'tachyons/src/_z-index'; -@import 'tachyons/src/_nested'; -/*@import 'tachyons/src/_styles';*/ - -/* Variables */ -/* Importing here will allow you to override any variables in the modules */ -@import 'tachyons/src/_colors'; -@import 'tachyons/src/_media-queries'; - -/* Debugging */ -/*@import 'tachyons/src/_debug-children'; -@import 'tachyons/src/_debug-grid';*/ - -/* Uncomment out the line below to help debug layout issues */ -/* @import 'tachyons/src/_debug'; */ diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_variables.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_variables.css deleted file mode 100644 index 8701b1530..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/_variables.css +++ /dev/null @@ -1,16 +0,0 @@ -:root { - --primary-color: #0594CB; - --primary-color-dark: #0A1922; - --primary-color-light: #f9f9f9; - --accent-color: #EBB951; - --accent-color-light: #FF4088; - --accent-color-dark: #33ba91; - --text-color-primary: #373737; - --text-on-primary-color: #fff; - --text-color-secondary: #ccc; - --text-color-disabled: #F7f7f7; - --divider-color: #f6f6f6; - --warn-color: red; - - --blue: var(--primary-color); -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/main.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/main.css deleted file mode 100644 index c71f69dd1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/css/main.css +++ /dev/null @@ -1,39 +0,0 @@ -/*Base Styles*/ -@import '_tachyons'; - -/* purgecss start ignore */ -@import '_header-link'; -@import '_animation'; -@import '_documentation-styles'; - -@import 'docsearch.js/dist/cdn/docsearch.min'; -@import '_carousel'; -@import '_code'; -@import '_tabs'; -@import '_color-scheme'; -@import '_columns'; -@import '_content'; -@import '_content-tables'; -@import '_definition-lists'; -@import '_fluid-type'; -@import '_font-family'; -@import '_hugo-internal-template-styling'; -@import '_no-js'; -@import '_social-icons'; -@import '_stickyheader'; - -@import '_svg'; -@import '_chroma'; -@import '_variables'; - -.nested-blockquote blockquote { - border-left: 4px solid var(--primary-color); - padding-left: 1em; - /*margin: 0;*/ -} - - -.mw-90 { - max-width:90%; -} -/* purgecss end ignore */ diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/bep-consulting.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/bep-consulting.svg deleted file mode 100644 index 5b1170f9b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/bep-consulting.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/linode-logo.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/linode-logo.svg deleted file mode 100644 index 7060e856f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/images/sponsors/linode-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/index.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/index.js deleted file mode 100644 index c89af041b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/index.js +++ /dev/null @@ -1,10 +0,0 @@ -require('typeface-muli'); -import styles from './css/main.css'; -import './js/clipboardjs.js'; -import './js/docsearch.js'; -import './js/lazysizes.js'; -import './js/menutoggle.js'; -import './js/scrolldir.js'; -import './js/smoothscroll.js'; -import './js/tabs.js'; -import './js/nojs.js'; diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/clipboardjs.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/clipboardjs.js deleted file mode 100644 index ffae31c7f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/clipboardjs.js +++ /dev/null @@ -1,30 +0,0 @@ -var Clipboard = require('clipboard/dist/clipboard.js'); -new Clipboard('.copy', { - target: function(trigger) { - if(trigger.classList.contains('copy-toggle')){ - return trigger.previousElementSibling; - } - return trigger.nextElementSibling; - } - }).on('success', function(e) { - successMessage(e.trigger, 'Copied!'); - e.clearSelection(); - }).on('error', function(e) { - successMessage(e.trigger, fallbackMessage(e.action)); -}); - -function successMessage(elem, msg) { - elem.setAttribute('class', 'copied bg-primary-color-dark f6 absolute top-0 right-0 lh-solid hover-bg-primary-color-dark bn white ph3 pv2'); - elem.setAttribute('aria-label', msg); -} - -function fallbackMessage(elem, action) { - var actionMsg = ''; - var actionKey = (action === 'cut' ? 'X' : 'C'); - if (isMac) { - actionMsg = 'Press ⌘-' + actionKey; - } else { - actionMsg = 'Press Ctrl-' + actionKey; - } - return actionMsg; -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/docsearch.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/docsearch.js deleted file mode 100644 index e14fb2994..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/docsearch.js +++ /dev/null @@ -1,8 +0,0 @@ -var docsearch = require('docsearch.js/dist/cdn/docsearch.js'); -docsearch({ - appId: 'D1BPLZHGYQ', - apiKey: '6df94e1e5d55d258c56f60d974d10314', - indexName: 'hugodocs', - inputSelector: '#search-input', - debug: true, // Set debug to true if you want to inspect the dropdown -}); diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/lazysizes.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/lazysizes.js deleted file mode 100644 index 4eb3950af..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/lazysizes.js +++ /dev/null @@ -1,3 +0,0 @@ -var lazysizes = require('lazysizes'); -// var lsnoscript = require('lazysizes/plugins/noscript/ls.noscript.js'); -var unveilhooks = require('lazysizes/plugins/unveilhooks/ls.unveilhooks.js'); diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/main.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/main.js deleted file mode 100644 index f6d3eac9f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/main.js +++ /dev/null @@ -1,22 +0,0 @@ -import styles from './../css/main.css'; -import './clipboardjs.js' -import './codeblocks.js' -import './docsearch.js' -import './lazysizes.js' -import './menutoggle.js' -import './scrolldir.js' -import './smoothscroll.js' -import './tabs.js' -import './nojs.js' - -// TO use jQuery, just call the modules you want -// var $ = require('jquery/src/core'); -// require('jquery/src/core/init'); -// require('jquery/src/manipulation'); - -// OR, use all of them -// var $ = require('jquery/src/jquery'); - -// And write your code -// $('body').append('

    Jquery is working

    '); -// diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/menutoggle.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/menutoggle.js deleted file mode 100644 index d0e645385..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/menutoggle.js +++ /dev/null @@ -1,31 +0,0 @@ -// Grab any element that has the 'js-toggle' class and add an event listner for the toggleClass function -var toggleBtns = document.getElementsByClassName('js-toggle') - for (var i = 0; i < toggleBtns.length; i++) { - toggleBtns[i].addEventListener('click', toggleClass, false) - } - -function toggleClass() { - // Define the data target via the dataset "target" (e.g. data-target=".docsmenu") - var content = this.dataset.target.split(' ') - // Find any menu items that are open - var mobileCurrentlyOpen = document.querySelector('.mobilemenu:not(.dn)') - var desktopCurrentlyOpen = document.querySelector('.desktopmenu:not(.dn)') - var desktopActive = document.querySelector('.desktopmenu:not(.dn)') - - // Loop through the targets' divs - for (var i = 0; i < content.length; i++) { - var matches = document.querySelectorAll(content[i]); - //for each, if the div has the 'dn' class (which is "display:none;"), remove it, otherwise, add that class - [].forEach.call(matches, function(dom) { - dom.classList.contains('dn') ? - dom.classList.remove('dn') : - dom.classList.add('dn'); - return false; - }); - // close the currently open menu items - if (mobileCurrentlyOpen) mobileCurrentlyOpen.classList.add('dn') - if (desktopCurrentlyOpen) desktopCurrentlyOpen.classList.add('dn') - if (desktopActive) desktopActive.classList.remove('db') - - } - } diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/nojs.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/nojs.js deleted file mode 100644 index 50b5126a9..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/nojs.js +++ /dev/null @@ -1 +0,0 @@ -document.documentElement.className = document.documentElement.className.replace(/\bno-js\b/, 'js'); diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/scrolldir.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/scrolldir.js deleted file mode 100644 index 0b69978cd..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/scrolldir.js +++ /dev/null @@ -1 +0,0 @@ -var scrollDir = require('scrolldir/dist/scrolldir.auto.min.js'); diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/smoothscroll.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/smoothscroll.js deleted file mode 100644 index 4bb2d99b8..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/smoothscroll.js +++ /dev/null @@ -1,80 +0,0 @@ -// query selector targets Hugo TOC -(function() { - - 'use strict'; - - // Feature Test - if ('querySelector' in document && 'addEventListener' in window && Array.prototype.forEach) { - - // Function to animate the scroll - var smoothScroll = function(anchor, duration) { - - // Calculate how far and how fast to scroll - var startLocation = window.pageYOffset; - var endLocation = anchor.offsetTop; - var distance = endLocation - startLocation; - var increments = distance / (duration / 16); - var stopAnimation; - - // Scroll the page by an increment, and check if it's time to stop - var animateScroll = function() { - window.scrollBy(0, increments); - stopAnimation(); - }; - - // If scrolling down - if (increments >= 0) { - // Stop animation when you reach the anchor OR the bottom of the page - stopAnimation = function() { - var travelled = window.pageYOffset; - if ((travelled >= (endLocation - increments)) || ((window.innerHeight + travelled) >= document.body.offsetHeight)) { - clearInterval(runAnimation); - } - }; - } - // If scrolling up - else { - // Stop animation when you reach the anchor OR the top of the page - stopAnimation = function() { - var travelled = window.pageYOffset; - if (travelled <= (endLocation || 0)) { - clearInterval(runAnimation); - } - }; - } - - // Loop the animation function - var runAnimation = setInterval(animateScroll, 16); - - }; - - // Define smooth scroll links - var scrollToggle = document.querySelectorAll('#TableOfContents ul li a'); - - // For each smooth scroll link - [].forEach.call(scrollToggle, function(toggle) { - - // When the smooth scroll link is clicked - toggle.addEventListener('click', function(e) { - - // Prevent the default link behavior - e.preventDefault(); - - // Get anchor link and calculate distance from the top - var dataID = toggle.getAttribute('href'); - var dataTarget = document.querySelector(dataID); - var dataSpeed = toggle.getAttribute('data-speed'); - - // If the anchor exists - if (dataTarget) { - // Scroll to the anchor - smoothScroll(dataTarget, dataSpeed || 500); - } - - }, false); - - }); - - } - -})(); diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/tabs.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/tabs.js deleted file mode 100644 index a689d474e..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/js/tabs.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Scripts which manages Code Toggle tabs. - */ -var i; -// store tabs variable -var allTabs = document.querySelectorAll("[data-toggle-tab]"); -var allPanes = document.querySelectorAll("[data-pane]"); - -function toggleTabs(event) { - - if(event.target){ - event.preventDefault(); - var clickedTab = event.currentTarget; - var targetKey = clickedTab.getAttribute("data-toggle-tab") - }else { - var targetKey = event - } - // We store the config language selected in users' localStorage - if(window.localStorage){ - window.localStorage.setItem("configLangPref", targetKey) - } - var selectedTabs = document.querySelectorAll("[data-toggle-tab='" + targetKey + "']"); - var selectedPanes = document.querySelectorAll("[data-pane='" + targetKey + "']"); - - for (var i = 0; i < allTabs.length; i++) { - allTabs[i].classList.remove("active"); - allPanes[i].classList.remove("active"); - } - - for (var i = 0; i < selectedTabs.length; i++) { - selectedTabs[i].classList.add("active"); - selectedPanes[i].classList.add("active"); - } - -} - -for (i = 0; i < allTabs.length; i++) { - allTabs[i].addEventListener("click", toggleTabs) -} -// Upon page load, if user has a preferred language in its localStorage, tabs are set to it. -if(window.localStorage.getItem('configLangPref')) { - toggleTabs(window.localStorage.getItem('configLangPref')) -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/css/app.css b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/css/app.css deleted file mode 100644 index 8b0c8c191..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/css/app.css +++ /dev/null @@ -1,5278 +0,0 @@ -/* muli-200normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 200; - src: - local('Muli Extra Light '), - local('Muli-Extra Light'), - url(/fonts/muli-latin-200.woff2) format('woff2'), - url(/fonts/muli-latin-200.woff) format('woff'); /* Modern Browsers */ -} -/* muli-200italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 200; - src: - local('Muli Extra Light italic'), - local('Muli-Extra Lightitalic'), - url(/fonts/muli-latin-200italic.woff2) format('woff2'), - url(/fonts/muli-latin-200italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-300normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 300; - src: - local('Muli Light '), - local('Muli-Light'), - url(/fonts/muli-latin-300.woff2) format('woff2'), - url(/fonts/muli-latin-300.woff) format('woff'); /* Modern Browsers */ -} -/* muli-300italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 300; - src: - local('Muli Light italic'), - local('Muli-Lightitalic'), - url(/fonts/muli-latin-300italic.woff2) format('woff2'), - url(/fonts/muli-latin-300italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-400normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 400; - src: - local('Muli Regular '), - local('Muli-Regular'), - url(/fonts/muli-latin-400.woff2) format('woff2'), - url(/fonts/muli-latin-400.woff) format('woff'); /* Modern Browsers */ -} -/* muli-400italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 400; - src: - local('Muli Regular italic'), - local('Muli-Regularitalic'), - url(/fonts/muli-latin-400italic.woff2) format('woff2'), - url(/fonts/muli-latin-400italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-600normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 600; - src: - local('Muli SemiBold '), - local('Muli-SemiBold'), - url(/fonts/muli-latin-600.woff2) format('woff2'), - url(/fonts/muli-latin-600.woff) format('woff'); /* Modern Browsers */ -} -/* muli-600italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 600; - src: - local('Muli SemiBold italic'), - local('Muli-SemiBolditalic'), - url(/fonts/muli-latin-600italic.woff2) format('woff2'), - url(/fonts/muli-latin-600italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-700normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 700; - src: - local('Muli Bold '), - local('Muli-Bold'), - url(/fonts/muli-latin-700.woff2) format('woff2'), - url(/fonts/muli-latin-700.woff) format('woff'); /* Modern Browsers */ -} -/* muli-700italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 700; - src: - local('Muli Bold italic'), - local('Muli-Bolditalic'), - url(/fonts/muli-latin-700italic.woff2) format('woff2'), - url(/fonts/muli-latin-700italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-800normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 800; - src: - local('Muli ExtraBold '), - local('Muli-ExtraBold'), - url(/fonts/muli-latin-800.woff2) format('woff2'), - url(/fonts/muli-latin-800.woff) format('woff'); /* Modern Browsers */ -} -/* muli-800italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 800; - src: - local('Muli ExtraBold italic'), - local('Muli-ExtraBolditalic'), - url(/fonts/muli-latin-800italic.woff2) format('woff2'), - url(/fonts/muli-latin-800italic.woff) format('woff'); /* Modern Browsers */ -} -/* muli-900normal - latin */ -@font-face { - font-family: 'Muli'; - font-style: normal; - font-display: swap; - font-weight: 900; - src: - local('Muli Black '), - local('Muli-Black'), - url(/fonts/muli-latin-900.woff2) format('woff2'), - url(/fonts/muli-latin-900.woff) format('woff'); /* Modern Browsers */ -} -/* muli-900italic - latin */ -@font-face { - font-family: 'Muli'; - font-style: italic; - font-display: swap; - font-weight: 900; - src: - local('Muli Black italic'), - local('Muli-Blackitalic'), - url(/fonts/muli-latin-900italic.woff2) format('woff2'), - url(/fonts/muli-latin-900italic.woff) format('woff'); /* Modern Browsers */ -} - - -/*Base Styles*/ -/*! TACHYONS v4.7.0 | http://tachyons.io */ -/* - * NOTE: The Tachyons folder is for backup/reference only. This file references the module - * ________ ______ - * ___ __/_____ _________ /______ ______________________ - * __ / _ __ `/ ___/_ __ \_ / / / __ \_ __ \_ ___/ - * _ / / /_/ // /__ _ / / / /_/ // /_/ / / / /(__ ) - * /_/ \__,_/ \___/ /_/ /_/_\__, / \____//_/ /_//____/ - * /____/ - * - * TABLE OF CONTENTS - * - * 1. External Library Includes - * - Normalize.css | http://normalize.css.github.io - * 2. Tachyons Modules - * 3. Variables - * - Media Queries - * - Colors - * 4. Debugging - * - Debug all - * - Debug children - * - */ -/* External Library Includes */ -/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ -/* Document - ========================================================================== */ -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} -/* Sections - ========================================================================== */ -/** - * Remove the margin in all browsers. - */ -body { - margin: 0; -} -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ -h1 { - font-size: 2em; - margin: 0.67em 0; -} -/* Grouping content - ========================================================================== */ -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ -hr { - -webkit-box-sizing: content-box; - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} -/* Text-level semantics - ========================================================================== */ -/** - * Remove the gray background on active links in IE 10. - */ -a { - background-color: transparent; -} -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; /* 2 */ -} -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ -b, -strong { - font-weight: bolder; -} -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} -/** - * Add the correct font size in all browsers. - */ -small { - font-size: 80%; -} -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} -sub { - bottom: -0.25em; -} -sup { - top: -0.5em; -} -/* Embedded content - ========================================================================== */ -/** - * Remove the border on images inside links in IE 10. - */ -img { - border-style: none; -} -/* Forms - ========================================================================== */ -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ -button, -input { /* 1 */ - overflow: visible; -} -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ -button, -select { /* 1 */ - text-transform: none; -} -/** - * Correct the inability to style clickable types in iOS and Safari. - */ -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} -/** - * Remove the inner border and padding in Firefox. - */ -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} -/** - * Restore the focus styles unset by the previous rule. - */ -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} -/** - * Correct the padding in Firefox. - */ -fieldset { - padding: 0.35em 0.75em 0.625em; -} -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ -legend { - -webkit-box-sizing: border-box; - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ -progress { - vertical-align: baseline; -} -/** - * Remove the default vertical scrollbar in IE 10+. - */ -textarea { - overflow: auto; -} -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ -[type="checkbox"], -[type="radio"] { - -webkit-box-sizing: border-box; - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} -/* Interactive - ========================================================================== */ -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ -details { - display: block; -} -/* - * Add the correct display in all browsers. - */ -summary { - display: list-item; -} -/* Misc - ========================================================================== */ -/** - * Add the correct display in IE 10+. - */ -template { - display: none; -} -/** - * Add the correct display in IE 10. - */ -[hidden] { - display: none; -} -/* Modules */ -/* - - BOX SIZING - -*/ -html, -body, -div, -article, -aside, -section, -main, -nav, -footer, -header, -form, -fieldset, -legend, -pre, -code, -a, -h1,h2,h3,h4,h5,h6, -p, -ul, -ol, -li, -dl, -dt, -dd, -blockquote, -figcaption, -figure, -textarea, -table, -td, -th, -tr, -input[type="email"], -input[type="number"], -input[type="password"], -input[type="tel"], -input[type="text"], -input[type="url"], -.border-box { - -webkit-box-sizing: border-box; - box-sizing: border-box; -} -/*@import 'tachyons/src/_aspect-ratios';*/ -/* - - IMAGES - Docs: http://tachyons.io/docs/elements/images/ - -*/ -/* Responsive images! */ -img { max-width: 100%; } -/* - - BACKGROUND SIZE - Docs: http://tachyons.io/docs/themes/background-size/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* - Often used in combination with background image set as an inline style - on an html element. -*/ -.cover { background-size: cover!important; } -.contain { background-size: contain!important; } -@media screen and (min-width: 30em) { - .cover-ns { background-size: cover!important; } - .contain-ns { background-size: contain!important; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .cover-m { background-size: cover!important; } - .contain-m { background-size: contain!important; } -} -@media screen and (min-width: 60em) { - .cover-l { background-size: cover!important; } - .contain-l { background-size: contain!important; } -} -/* - - BACKGROUND POSITION - - Base: - bg = background - - Modifiers: - -center = center center - -top = top center - -right = center right - -bottom = bottom center - -left = center left - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - - */ -.bg-center { - background-repeat: no-repeat; - background-position: center center; -} -.bg-top { - background-repeat: no-repeat; - background-position: top center; -} -.bg-right { - background-repeat: no-repeat; - background-position: center right; -} -.bg-bottom { - background-repeat: no-repeat; - background-position: bottom center; -} -.bg-left { - background-repeat: no-repeat; - background-position: center left; -} -@media screen and (min-width: 30em) { - .bg-center-ns { - background-repeat: no-repeat; - background-position: center center; - } - - .bg-top-ns { - background-repeat: no-repeat; - background-position: top center; - } - - .bg-right-ns { - background-repeat: no-repeat; - background-position: center right; - } - - .bg-bottom-ns { - background-repeat: no-repeat; - background-position: bottom center; - } - - .bg-left-ns { - background-repeat: no-repeat; - background-position: center left; - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .bg-center-m { - background-repeat: no-repeat; - background-position: center center; - } - - .bg-top-m { - background-repeat: no-repeat; - background-position: top center; - } - - .bg-right-m { - background-repeat: no-repeat; - background-position: center right; - } - - .bg-bottom-m { - background-repeat: no-repeat; - background-position: bottom center; - } - - .bg-left-m { - background-repeat: no-repeat; - background-position: center left; - } -} -@media screen and (min-width: 60em) { - .bg-center-l { - background-repeat: no-repeat; - background-position: center center; - } - - .bg-top-l { - background-repeat: no-repeat; - background-position: top center; - } - - .bg-right-l { - background-repeat: no-repeat; - background-position: center right; - } - - .bg-bottom-l { - background-repeat: no-repeat; - background-position: bottom center; - } - - .bg-left-l { - background-repeat: no-repeat; - background-position: center left; - } -} -/*@import 'tachyons/src/_outlines';*/ -/* - - BORDERS - Docs: http://tachyons.io/docs/themes/borders/ - - Base: - b = border - - Modifiers: - a = all - t = top - r = right - b = bottom - l = left - n = none - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.ba { border-style: solid; border-width: 1px; } -.bt { border-top-style: solid; border-top-width: 1px; } -.br { border-right-style: solid; border-right-width: 1px; } -.bb { border-bottom-style: solid; border-bottom-width: 1px; } -.bl { border-left-style: solid; border-left-width: 1px; } -.bn { border-style: none; border-width: 0; } -@media screen and (min-width: 30em) { - .ba-ns { border-style: solid; border-width: 1px; } - .bt-ns { border-top-style: solid; border-top-width: 1px; } - .br-ns { border-right-style: solid; border-right-width: 1px; } - .bb-ns { border-bottom-style: solid; border-bottom-width: 1px; } - .bl-ns { border-left-style: solid; border-left-width: 1px; } - .bn-ns { border-style: none; border-width: 0; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .ba-m { border-style: solid; border-width: 1px; } - .bt-m { border-top-style: solid; border-top-width: 1px; } - .br-m { border-right-style: solid; border-right-width: 1px; } - .bb-m { border-bottom-style: solid; border-bottom-width: 1px; } - .bl-m { border-left-style: solid; border-left-width: 1px; } - .bn-m { border-style: none; border-width: 0; } -} -@media screen and (min-width: 60em) { - .ba-l { border-style: solid; border-width: 1px; } - .bt-l { border-top-style: solid; border-top-width: 1px; } - .br-l { border-right-style: solid; border-right-width: 1px; } - .bb-l { border-bottom-style: solid; border-bottom-width: 1px; } - .bl-l { border-left-style: solid; border-left-width: 1px; } - .bn-l { border-style: none; border-width: 0; } -} -/* - - BORDER COLORS - Docs: http://tachyons.io/docs/themes/borders/ - - Border colors can be used to extend the base - border classes ba,bt,bb,br,bl found in the _borders.css file. - - The base border class by default will set the color of the border - to that of the current text color. These classes are for the cases - where you desire for the text and border colors to be different. - - Base: - b = border - - Modifiers: - --color-name = each color variable name is also a border color name - -*/ -.b--black { border-color: #000; } -.b--near-black { border-color: #111; } -.b--dark-gray { border-color: #333; } -.b--mid-gray { border-color: #555; } -.b--gray { border-color: #777; } -.b--silver { border-color: #999; } -.b--light-silver { border-color: #aaa; } -.b--moon-gray { border-color: #ccc; } -.b--light-gray { border-color: #eee; } -.b--near-white { border-color: #f4f4f4; } -.b--white { border-color: #fff; } -.b--white-90 { border-color: rgba(255, 255, 255, .9); } -.b--white-80 { border-color: rgba(255, 255, 255, .8); } -.b--white-70 { border-color: rgba(255, 255, 255, .7); } -.b--white-60 { border-color: rgba(255, 255, 255, .6); } -.b--white-50 { border-color: rgba(255, 255, 255, .5); } -.b--white-40 { border-color: rgba(255, 255, 255, .4); } -.b--white-30 { border-color: rgba(255, 255, 255, .3); } -.b--white-20 { border-color: rgba(255, 255, 255, .2); } -.b--white-10 { border-color: rgba(255, 255, 255, .1); } -.b--white-05 { border-color: rgba(255, 255, 255, .05); } -.b--white-025 { border-color: rgba(255, 255, 255, .025); } -.b--white-0125 { border-color: rgba(255, 255, 255, .0125); } -.b--black-90 { border-color: rgba(0, 0, 0, .9); } -.b--black-80 { border-color: rgba(0, 0, 0, .8); } -.b--black-70 { border-color: rgba(0, 0, 0, .7); } -.b--black-60 { border-color: rgba(0, 0, 0, .6); } -.b--black-50 { border-color: rgba(0, 0, 0, .5); } -.b--black-40 { border-color: rgba(0, 0, 0, .4); } -.b--black-30 { border-color: rgba(0, 0, 0, .3); } -.b--black-20 { border-color: rgba(0, 0, 0, .2); } -.b--black-10 { border-color: rgba(0, 0, 0, .1); } -.b--black-05 { border-color: rgba(0, 0, 0, .05); } -.b--black-025 { border-color: rgba(0, 0, 0, .025); } -.b--black-0125 { border-color: rgba(0, 0, 0, .0125); } -.b--dark-red { border-color: #e7040f; } -.b--red { border-color: #ff4136; } -.b--light-red { border-color: #ff725c; } -.b--orange { border-color: #ff6300; } -.b--gold { border-color: #ffb700; } -.b--yellow { border-color: #ffd700; } -.b--light-yellow { border-color: #fbf1a9; } -.b--purple { border-color: #5e2ca5; } -.b--light-purple { border-color: #a463f2; } -.b--dark-pink { border-color: #d5008f; } -.b--hot-pink { border-color: #ff41b4; } -.b--pink { border-color: #ff80cc; } -.b--light-pink { border-color: #ffa3d7; } -.b--dark-green { border-color: #137752; } -.b--green { border-color: #19a974; } -.b--light-green { border-color: #9eebcf; } -.b--navy { border-color: #001b44; } -.b--dark-blue { border-color: #00449e; } -.b--blue { border-color: #0594CB; } -.b--light-blue { border-color: #96ccff; } -.b--lightest-blue { border-color: #cdecff; } -.b--washed-blue { border-color: #f6fffe; } -.b--washed-green { border-color: #e8fdf5; } -.b--washed-yellow { border-color: #fffceb; } -.b--washed-red { border-color: #ffdfdf; } -.b--transparent { border-color: transparent; } -.b--inherit { border-color: inherit; } -/* - - BORDER RADIUS - Docs: http://tachyons.io/docs/themes/border-radius/ - - Base: - br = border-radius - - Modifiers: - 0 = 0/none - 1 = 1st step in scale - 2 = 2nd step in scale - 3 = 3rd step in scale - 4 = 4th step in scale - - Literal values: - -100 = 100% - -pill = 9999px - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.br0 { border-radius: 0; } -.br1 { border-radius: .125rem; } -.br2 { border-radius: .25rem; } -.br3 { border-radius: .5rem; } -.br4 { border-radius: 1rem; } -.br-100 { border-radius: 100%; } -.br-pill { border-radius: 9999px; } -.br--bottom { - border-top-left-radius: 0; - border-top-right-radius: 0; - } -.br--top { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } -.br--right { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } -.br--left { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -@media screen and (min-width: 30em) { - .br0-ns { border-radius: 0; } - .br1-ns { border-radius: .125rem; } - .br2-ns { border-radius: .25rem; } - .br3-ns { border-radius: .5rem; } - .br4-ns { border-radius: 1rem; } - .br-100-ns { border-radius: 100%; } - .br-pill-ns { border-radius: 9999px; } - .br--bottom-ns { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - .br--top-ns { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - .br--right-ns { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - .br--left-ns { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .br0-m { border-radius: 0; } - .br1-m { border-radius: .125rem; } - .br2-m { border-radius: .25rem; } - .br3-m { border-radius: .5rem; } - .br4-m { border-radius: 1rem; } - .br-100-m { border-radius: 100%; } - .br-pill-m { border-radius: 9999px; } - .br--bottom-m { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - .br--top-m { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - .br--right-m { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - .br--left-m { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -} -@media screen and (min-width: 60em) { - .br0-l { border-radius: 0; } - .br1-l { border-radius: .125rem; } - .br2-l { border-radius: .25rem; } - .br3-l { border-radius: .5rem; } - .br4-l { border-radius: 1rem; } - .br-100-l { border-radius: 100%; } - .br-pill-l { border-radius: 9999px; } - .br--bottom-l { - border-top-left-radius: 0; - border-top-right-radius: 0; - } - .br--top-l { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - .br--right-l { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - .br--left-l { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } -} -/* - - BORDER STYLES - Docs: http://tachyons.io/docs/themes/borders/ - - Depends on base border module in _borders.css - - Base: - b = border-style - - Modifiers: - --none = none - --dotted = dotted - --dashed = dashed - --solid = solid - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - - */ -.b--dotted { border-style: dotted; } -.b--dashed { border-style: dashed; } -.b--solid { border-style: solid; } -.b--none { border-style: none; } -@media screen and (min-width: 30em) { - .b--dotted-ns { border-style: dotted; } - .b--dashed-ns { border-style: dashed; } - .b--solid-ns { border-style: solid; } - .b--none-ns { border-style: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .b--dotted-m { border-style: dotted; } - .b--dashed-m { border-style: dashed; } - .b--solid-m { border-style: solid; } - .b--none-m { border-style: none; } -} -@media screen and (min-width: 60em) { - .b--dotted-l { border-style: dotted; } - .b--dashed-l { border-style: dashed; } - .b--solid-l { border-style: solid; } - .b--none-l { border-style: none; } -} -/* - - BORDER WIDTHS - Docs: http://tachyons.io/docs/themes/borders/ - - Base: - bw = border-width - - Modifiers: - 0 = 0 width border - 1 = 1st step in border-width scale - 2 = 2nd step in border-width scale - 3 = 3rd step in border-width scale - 4 = 4th step in border-width scale - 5 = 5th step in border-width scale - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.bw0 { border-width: 0; } -.bw1 { border-width: .125rem; } -.bw2 { border-width: .25rem; } -.bw3 { border-width: .5rem; } -.bw4 { border-width: 1rem; } -.bw5 { border-width: 2rem; } -/* Resets */ -.bt-0 { border-top-width: 0; } -.br-0 { border-right-width: 0; } -.bb-0 { border-bottom-width: 0; } -.bl-0 { border-left-width: 0; } -@media screen and (min-width: 30em) { - .bw0-ns { border-width: 0; } - .bw1-ns { border-width: .125rem; } - .bw2-ns { border-width: .25rem; } - .bw3-ns { border-width: .5rem; } - .bw4-ns { border-width: 1rem; } - .bw5-ns { border-width: 2rem; } - .bt-0-ns { border-top-width: 0; } - .br-0-ns { border-right-width: 0; } - .bb-0-ns { border-bottom-width: 0; } - .bl-0-ns { border-left-width: 0; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .bw0-m { border-width: 0; } - .bw1-m { border-width: .125rem; } - .bw2-m { border-width: .25rem; } - .bw3-m { border-width: .5rem; } - .bw4-m { border-width: 1rem; } - .bw5-m { border-width: 2rem; } - .bt-0-m { border-top-width: 0; } - .br-0-m { border-right-width: 0; } - .bb-0-m { border-bottom-width: 0; } - .bl-0-m { border-left-width: 0; } -} -@media screen and (min-width: 60em) { - .bw0-l { border-width: 0; } - .bw1-l { border-width: .125rem; } - .bw2-l { border-width: .25rem; } - .bw3-l { border-width: .5rem; } - .bw4-l { border-width: 1rem; } - .bw5-l { border-width: 2rem; } - .bt-0-l { border-top-width: 0; } - .br-0-l { border-right-width: 0; } - .bb-0-l { border-bottom-width: 0; } - .bl-0-l { border-left-width: 0; } -} -/* - - BOX-SHADOW - Docs: http://tachyons.io/docs/themes/box-shadow/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - - */ -.shadow-1 { -webkit-box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); } -.shadow-2 { -webkit-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); } -.shadow-3 { -webkit-box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); } -.shadow-4 { -webkit-box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); } -.shadow-5 { -webkit-box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); } -@media screen and (min-width: 30em) { - .shadow-1-ns { -webkit-box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); } - .shadow-2-ns { -webkit-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); } - .shadow-3-ns { -webkit-box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); } - .shadow-4-ns { -webkit-box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); } - .shadow-5-ns { -webkit-box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .shadow-1-m { -webkit-box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); } - .shadow-2-m { -webkit-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); } - .shadow-3-m { -webkit-box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); } - .shadow-4-m { -webkit-box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); } - .shadow-5-m { -webkit-box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); } -} -@media screen and (min-width: 60em) { - .shadow-1-l { -webkit-box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, .2); } - .shadow-2-l { -webkit-box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); box-shadow: 0px 0px 8px 2px rgba(0, 0, 0, .2); } - .shadow-3-l { -webkit-box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); box-shadow: 2px 2px 4px 2px rgba(0, 0, 0, .2); } - .shadow-4-l { -webkit-box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, .2); } - .shadow-5-l { -webkit-box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); box-shadow: 4px 4px 8px 0px rgba(0, 0, 0, .2); } -} -/*@import 'tachyons/src/_code';*/ -/* - - COORDINATES - Docs: http://tachyons.io/docs/layout/position/ - - Use in combination with the position module. - - Base: - top - bottom - right - left - - Modifiers: - -0 = literal value 0 - -1 = literal value 1 - -2 = literal value 2 - --1 = literal value -1 - --2 = literal value -2 - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.top-0 { top: 0; } -.right-0 { right: 0; } -.bottom-0 { bottom: 0; } -.left-0 { left: 0; } -.top-1 { top: 1rem; } -.right-1 { right: 1rem; } -.bottom-1 { bottom: 1rem; } -.left-1 { left: 1rem; } -.top-2 { top: 2rem; } -.right-2 { right: 2rem; } -.bottom-2 { bottom: 2rem; } -.left-2 { left: 2rem; } -.top--1 { top: -1rem; } -.right--1 { right: -1rem; } -.bottom--1 { bottom: -1rem; } -.left--1 { left: -1rem; } -.top--2 { top: -2rem; } -.right--2 { right: -2rem; } -.bottom--2 { bottom: -2rem; } -.left--2 { left: -2rem; } -.absolute--fill { - top: 0; - right: 0; - bottom: 0; - left: 0; -} -@media screen and (min-width: 30em) { - .top-0-ns { top: 0; } - .left-0-ns { left: 0; } - .right-0-ns { right: 0; } - .bottom-0-ns { bottom: 0; } - .top-1-ns { top: 1rem; } - .left-1-ns { left: 1rem; } - .right-1-ns { right: 1rem; } - .bottom-1-ns { bottom: 1rem; } - .top-2-ns { top: 2rem; } - .left-2-ns { left: 2rem; } - .right-2-ns { right: 2rem; } - .bottom-2-ns { bottom: 2rem; } - .top--1-ns { top: -1rem; } - .right--1-ns { right: -1rem; } - .bottom--1-ns { bottom: -1rem; } - .left--1-ns { left: -1rem; } - .top--2-ns { top: -2rem; } - .right--2-ns { right: -2rem; } - .bottom--2-ns { bottom: -2rem; } - .left--2-ns { left: -2rem; } - .absolute--fill-ns { - top: 0; - right: 0; - bottom: 0; - left: 0; - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .top-0-m { top: 0; } - .left-0-m { left: 0; } - .right-0-m { right: 0; } - .bottom-0-m { bottom: 0; } - .top-1-m { top: 1rem; } - .left-1-m { left: 1rem; } - .right-1-m { right: 1rem; } - .bottom-1-m { bottom: 1rem; } - .top-2-m { top: 2rem; } - .left-2-m { left: 2rem; } - .right-2-m { right: 2rem; } - .bottom-2-m { bottom: 2rem; } - .top--1-m { top: -1rem; } - .right--1-m { right: -1rem; } - .bottom--1-m { bottom: -1rem; } - .left--1-m { left: -1rem; } - .top--2-m { top: -2rem; } - .right--2-m { right: -2rem; } - .bottom--2-m { bottom: -2rem; } - .left--2-m { left: -2rem; } - .absolute--fill-m { - top: 0; - right: 0; - bottom: 0; - left: 0; - } -} -@media screen and (min-width: 60em) { - .top-0-l { top: 0; } - .left-0-l { left: 0; } - .right-0-l { right: 0; } - .bottom-0-l { bottom: 0; } - .top-1-l { top: 1rem; } - .left-1-l { left: 1rem; } - .right-1-l { right: 1rem; } - .bottom-1-l { bottom: 1rem; } - .top-2-l { top: 2rem; } - .left-2-l { left: 2rem; } - .right-2-l { right: 2rem; } - .bottom-2-l { bottom: 2rem; } - .top--1-l { top: -1rem; } - .right--1-l { right: -1rem; } - .bottom--1-l { bottom: -1rem; } - .left--1-l { left: -1rem; } - .top--2-l { top: -2rem; } - .right--2-l { right: -2rem; } - .bottom--2-l { bottom: -2rem; } - .left--2-l { left: -2rem; } - .absolute--fill-l { - top: 0; - right: 0; - bottom: 0; - left: 0; - } -} -/* - - CLEARFIX - http://tachyons.io/docs/layout/clearfix/ - -*/ -/* Nicolas Gallaghers Clearfix solution - Ref: http://nicolasgallagher.com/micro-clearfix-hack/ */ -.cf:before, -.cf:after { content: " "; display: table; } -.cf:after { clear: both; } -.cf { *zoom: 1; } -.cl { clear: left; } -.cr { clear: right; } -.cb { clear: both; } -.cn { clear: none; } -@media screen and (min-width: 30em) { - .cl-ns { clear: left; } - .cr-ns { clear: right; } - .cb-ns { clear: both; } - .cn-ns { clear: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .cl-m { clear: left; } - .cr-m { clear: right; } - .cb-m { clear: both; } - .cn-m { clear: none; } -} -@media screen and (min-width: 60em) { - .cl-l { clear: left; } - .cr-l { clear: right; } - .cb-l { clear: both; } - .cn-l { clear: none; } -} -/* - - DISPLAY - Docs: http://tachyons.io/docs/layout/display - - Base: - d = display - - Modifiers: - n = none - b = block - ib = inline-block - it = inline-table - t = table - tc = table-cell - t-row = table-row - t-columm = table-column - t-column-group = table-column-group - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.dn { display: none; } -.di { display: inline; } -.db { display: block; } -.dib { display: inline-block; } -.dit { display: inline-table; } -.dt { display: table; } -.dtc { display: table-cell; } -.dt-row { display: table-row; } -.dt-row-group { display: table-row-group; } -.dt-column { display: table-column; } -.dt-column-group { display: table-column-group; } -/* - This will set table to full width and then - all cells will be equal width -*/ -.dt--fixed { - table-layout: fixed; - width: 100%; -} -@media screen and (min-width: 30em) { - .dn-ns { display: none; } - .di-ns { display: inline; } - .db-ns { display: block; } - .dib-ns { display: inline-block; } - .dit-ns { display: inline-table; } - .dt-ns { display: table; } - .dtc-ns { display: table-cell; } - .dt-row-ns { display: table-row; } - .dt-row-group-ns { display: table-row-group; } - .dt-column-ns { display: table-column; } - .dt-column-group-ns { display: table-column-group; } - - .dt--fixed-ns { - table-layout: fixed; - width: 100%; - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .dn-m { display: none; } - .di-m { display: inline; } - .db-m { display: block; } - .dib-m { display: inline-block; } - .dit-m { display: inline-table; } - .dt-m { display: table; } - .dtc-m { display: table-cell; } - .dt-row-m { display: table-row; } - .dt-row-group-m { display: table-row-group; } - .dt-column-m { display: table-column; } - .dt-column-group-m { display: table-column-group; } - - .dt--fixed-m { - table-layout: fixed; - width: 100%; - } -} -@media screen and (min-width: 60em) { - .dn-l { display: none; } - .di-l { display: inline; } - .db-l { display: block; } - .dib-l { display: inline-block; } - .dit-l { display: inline-table; } - .dt-l { display: table; } - .dtc-l { display: table-cell; } - .dt-row-l { display: table-row; } - .dt-row-group-l { display: table-row-group; } - .dt-column-l { display: table-column; } - .dt-column-group-l { display: table-column-group; } - - .dt--fixed-l { - table-layout: fixed; - width: 100%; - } -} -/* - - FLEXBOX - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.flex { display: -webkit-box; display: -ms-flexbox; display: flex; } -.inline-flex { display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; } -/* 1. Fix for Chrome 44 bug. - * https://code.google.com/p/chromium/issues/detail?id=506893 */ -.flex-auto { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-width: 0; /* 1 */ - min-height: 0; /* 1 */ -} -.flex-none { -webkit-box-flex: 0; -ms-flex: none; flex: none; } -.flex-column { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; } -.flex-row { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -ms-flex-direction: row; flex-direction: row; } -.flex-wrap { -ms-flex-wrap: wrap; flex-wrap: wrap; } -.flex-nowrap { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } -.flex-wrap-reverse { -ms-flex-wrap: wrap-reverse; flex-wrap: wrap-reverse; } -.flex-column-reverse { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -ms-flex-direction: column-reverse; flex-direction: column-reverse; } -.flex-row-reverse { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } -.items-start { -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; } -.items-end { -webkit-box-align: end; -ms-flex-align: end; align-items: flex-end; } -.items-center { -webkit-box-align: center; -ms-flex-align: center; align-items: center; } -.items-baseline { -webkit-box-align: baseline; -ms-flex-align: baseline; align-items: baseline; } -.items-stretch { -webkit-box-align: stretch; -ms-flex-align: stretch; align-items: stretch; } -.self-start { -ms-flex-item-align: start; align-self: flex-start; } -.self-end { -ms-flex-item-align: end; align-self: flex-end; } -.self-center { -ms-flex-item-align: center; align-self: center; } -.self-baseline { -ms-flex-item-align: baseline; align-self: baseline; } -.self-stretch { -ms-flex-item-align: stretch; align-self: stretch; } -.justify-start { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; } -.justify-end { -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; } -.justify-center { -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } -.justify-between { -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; } -.justify-around { -ms-flex-pack: distribute; justify-content: space-around; } -.content-start { -ms-flex-line-pack: start; align-content: flex-start; } -.content-end { -ms-flex-line-pack: end; align-content: flex-end; } -.content-center { -ms-flex-line-pack: center; align-content: center; } -.content-between { -ms-flex-line-pack: justify; align-content: space-between; } -.content-around { -ms-flex-line-pack: distribute; align-content: space-around; } -.content-stretch { -ms-flex-line-pack: stretch; align-content: stretch; } -.order-0 { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; } -.order-1 { -webkit-box-ordinal-group: 2; -ms-flex-order: 1; order: 1; } -.order-2 { -webkit-box-ordinal-group: 3; -ms-flex-order: 2; order: 2; } -.order-3 { -webkit-box-ordinal-group: 4; -ms-flex-order: 3; order: 3; } -.order-4 { -webkit-box-ordinal-group: 5; -ms-flex-order: 4; order: 4; } -.order-5 { -webkit-box-ordinal-group: 6; -ms-flex-order: 5; order: 5; } -.order-6 { -webkit-box-ordinal-group: 7; -ms-flex-order: 6; order: 6; } -.order-7 { -webkit-box-ordinal-group: 8; -ms-flex-order: 7; order: 7; } -.order-8 { -webkit-box-ordinal-group: 9; -ms-flex-order: 8; order: 8; } -.order-last { -webkit-box-ordinal-group: 100000; -ms-flex-order: 99999; order: 99999; } -.flex-grow-0 { -webkit-box-flex: 0; -ms-flex-positive: 0; flex-grow: 0; } -.flex-grow-1 { -webkit-box-flex: 1; -ms-flex-positive: 1; flex-grow: 1; } -.flex-shrink-0 { -ms-flex-negative: 0; flex-shrink: 0; } -.flex-shrink-1 { -ms-flex-negative: 1; flex-shrink: 1; } -@media screen and (min-width: 30em) { - .flex-ns { display: -webkit-box; display: -ms-flexbox; display: flex; } - .inline-flex-ns { display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; } - .flex-auto-ns { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-width: 0; /* 1 */ - min-height: 0; /* 1 */ - } - .flex-none-ns { -webkit-box-flex: 0; -ms-flex: none; flex: none; } - .flex-column-ns { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; } - .flex-row-ns { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -ms-flex-direction: row; flex-direction: row; } - .flex-wrap-ns { -ms-flex-wrap: wrap; flex-wrap: wrap; } - .flex-nowrap-ns { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } - .flex-wrap-reverse-ns { -ms-flex-wrap: wrap-reverse; flex-wrap: wrap-reverse; } - .flex-column-reverse-ns { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -ms-flex-direction: column-reverse; flex-direction: column-reverse; } - .flex-row-reverse-ns { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } - .items-start-ns { -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; } - .items-end-ns { -webkit-box-align: end; -ms-flex-align: end; align-items: flex-end; } - .items-center-ns { -webkit-box-align: center; -ms-flex-align: center; align-items: center; } - .items-baseline-ns { -webkit-box-align: baseline; -ms-flex-align: baseline; align-items: baseline; } - .items-stretch-ns { -webkit-box-align: stretch; -ms-flex-align: stretch; align-items: stretch; } - - .self-start-ns { -ms-flex-item-align: start; align-self: flex-start; } - .self-end-ns { -ms-flex-item-align: end; align-self: flex-end; } - .self-center-ns { -ms-flex-item-align: center; align-self: center; } - .self-baseline-ns { -ms-flex-item-align: baseline; align-self: baseline; } - .self-stretch-ns { -ms-flex-item-align: stretch; align-self: stretch; } - - .justify-start-ns { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; } - .justify-end-ns { -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; } - .justify-center-ns { -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } - .justify-between-ns { -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; } - .justify-around-ns { -ms-flex-pack: distribute; justify-content: space-around; } - - .content-start-ns { -ms-flex-line-pack: start; align-content: flex-start; } - .content-end-ns { -ms-flex-line-pack: end; align-content: flex-end; } - .content-center-ns { -ms-flex-line-pack: center; align-content: center; } - .content-between-ns { -ms-flex-line-pack: justify; align-content: space-between; } - .content-around-ns { -ms-flex-line-pack: distribute; align-content: space-around; } - .content-stretch-ns { -ms-flex-line-pack: stretch; align-content: stretch; } - - .order-0-ns { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; } - .order-1-ns { -webkit-box-ordinal-group: 2; -ms-flex-order: 1; order: 1; } - .order-2-ns { -webkit-box-ordinal-group: 3; -ms-flex-order: 2; order: 2; } - .order-3-ns { -webkit-box-ordinal-group: 4; -ms-flex-order: 3; order: 3; } - .order-4-ns { -webkit-box-ordinal-group: 5; -ms-flex-order: 4; order: 4; } - .order-5-ns { -webkit-box-ordinal-group: 6; -ms-flex-order: 5; order: 5; } - .order-6-ns { -webkit-box-ordinal-group: 7; -ms-flex-order: 6; order: 6; } - .order-7-ns { -webkit-box-ordinal-group: 8; -ms-flex-order: 7; order: 7; } - .order-8-ns { -webkit-box-ordinal-group: 9; -ms-flex-order: 8; order: 8; } - .order-last-ns { -webkit-box-ordinal-group: 100000; -ms-flex-order: 99999; order: 99999; } - - .flex-grow-0-ns { -webkit-box-flex: 0; -ms-flex-positive: 0; flex-grow: 0; } - .flex-grow-1-ns { -webkit-box-flex: 1; -ms-flex-positive: 1; flex-grow: 1; } - - .flex-shrink-0-ns { -ms-flex-negative: 0; flex-shrink: 0; } - .flex-shrink-1-ns { -ms-flex-negative: 1; flex-shrink: 1; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .flex-m { display: -webkit-box; display: -ms-flexbox; display: flex; } - .inline-flex-m { display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; } - .flex-auto-m { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-width: 0; /* 1 */ - min-height: 0; /* 1 */ - } - .flex-none-m { -webkit-box-flex: 0; -ms-flex: none; flex: none; } - .flex-column-m { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; } - .flex-row-m { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -ms-flex-direction: row; flex-direction: row; } - .flex-wrap-m { -ms-flex-wrap: wrap; flex-wrap: wrap; } - .flex-nowrap-m { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } - .flex-wrap-reverse-m { -ms-flex-wrap: wrap-reverse; flex-wrap: wrap-reverse; } - .flex-column-reverse-m { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -ms-flex-direction: column-reverse; flex-direction: column-reverse; } - .flex-row-reverse-m { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } - .items-start-m { -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; } - .items-end-m { -webkit-box-align: end; -ms-flex-align: end; align-items: flex-end; } - .items-center-m { -webkit-box-align: center; -ms-flex-align: center; align-items: center; } - .items-baseline-m { -webkit-box-align: baseline; -ms-flex-align: baseline; align-items: baseline; } - .items-stretch-m { -webkit-box-align: stretch; -ms-flex-align: stretch; align-items: stretch; } - - .self-start-m { -ms-flex-item-align: start; align-self: flex-start; } - .self-end-m { -ms-flex-item-align: end; align-self: flex-end; } - .self-center-m { -ms-flex-item-align: center; align-self: center; } - .self-baseline-m { -ms-flex-item-align: baseline; align-self: baseline; } - .self-stretch-m { -ms-flex-item-align: stretch; align-self: stretch; } - - .justify-start-m { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; } - .justify-end-m { -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; } - .justify-center-m { -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } - .justify-between-m { -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; } - .justify-around-m { -ms-flex-pack: distribute; justify-content: space-around; } - - .content-start-m { -ms-flex-line-pack: start; align-content: flex-start; } - .content-end-m { -ms-flex-line-pack: end; align-content: flex-end; } - .content-center-m { -ms-flex-line-pack: center; align-content: center; } - .content-between-m { -ms-flex-line-pack: justify; align-content: space-between; } - .content-around-m { -ms-flex-line-pack: distribute; align-content: space-around; } - .content-stretch-m { -ms-flex-line-pack: stretch; align-content: stretch; } - - .order-0-m { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; } - .order-1-m { -webkit-box-ordinal-group: 2; -ms-flex-order: 1; order: 1; } - .order-2-m { -webkit-box-ordinal-group: 3; -ms-flex-order: 2; order: 2; } - .order-3-m { -webkit-box-ordinal-group: 4; -ms-flex-order: 3; order: 3; } - .order-4-m { -webkit-box-ordinal-group: 5; -ms-flex-order: 4; order: 4; } - .order-5-m { -webkit-box-ordinal-group: 6; -ms-flex-order: 5; order: 5; } - .order-6-m { -webkit-box-ordinal-group: 7; -ms-flex-order: 6; order: 6; } - .order-7-m { -webkit-box-ordinal-group: 8; -ms-flex-order: 7; order: 7; } - .order-8-m { -webkit-box-ordinal-group: 9; -ms-flex-order: 8; order: 8; } - .order-last-m { -webkit-box-ordinal-group: 100000; -ms-flex-order: 99999; order: 99999; } - - .flex-grow-0-m { -webkit-box-flex: 0; -ms-flex-positive: 0; flex-grow: 0; } - .flex-grow-1-m { -webkit-box-flex: 1; -ms-flex-positive: 1; flex-grow: 1; } - - .flex-shrink-0-m { -ms-flex-negative: 0; flex-shrink: 0; } - .flex-shrink-1-m { -ms-flex-negative: 1; flex-shrink: 1; } -} -@media screen and (min-width: 60em) { - .flex-l { display: -webkit-box; display: -ms-flexbox; display: flex; } - .inline-flex-l { display: -webkit-inline-box; display: -ms-inline-flexbox; display: inline-flex; } - .flex-auto-l { - -webkit-box-flex: 1; - -ms-flex: 1 1 auto; - flex: 1 1 auto; - min-width: 0; /* 1 */ - min-height: 0; /* 1 */ - } - .flex-none-l { -webkit-box-flex: 0; -ms-flex: none; flex: none; } - .flex-column-l { -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; flex-direction: column; } - .flex-row-l { -webkit-box-orient: horizontal; -webkit-box-direction: normal; -ms-flex-direction: row; flex-direction: row; } - .flex-wrap-l { -ms-flex-wrap: wrap; flex-wrap: wrap; } - .flex-nowrap-l { -ms-flex-wrap: nowrap; flex-wrap: nowrap; } - .flex-wrap-reverse-l { -ms-flex-wrap: wrap-reverse; flex-wrap: wrap-reverse; } - .flex-column-reverse-l { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -ms-flex-direction: column-reverse; flex-direction: column-reverse; } - .flex-row-reverse-l { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } - - .items-start-l { -webkit-box-align: start; -ms-flex-align: start; align-items: flex-start; } - .items-end-l { -webkit-box-align: end; -ms-flex-align: end; align-items: flex-end; } - .items-center-l { -webkit-box-align: center; -ms-flex-align: center; align-items: center; } - .items-baseline-l { -webkit-box-align: baseline; -ms-flex-align: baseline; align-items: baseline; } - .items-stretch-l { -webkit-box-align: stretch; -ms-flex-align: stretch; align-items: stretch; } - - .self-start-l { -ms-flex-item-align: start; align-self: flex-start; } - .self-end-l { -ms-flex-item-align: end; align-self: flex-end; } - .self-center-l { -ms-flex-item-align: center; align-self: center; } - .self-baseline-l { -ms-flex-item-align: baseline; align-self: baseline; } - .self-stretch-l { -ms-flex-item-align: stretch; align-self: stretch; } - - .justify-start-l { -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; } - .justify-end-l { -webkit-box-pack: end; -ms-flex-pack: end; justify-content: flex-end; } - .justify-center-l { -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; } - .justify-between-l { -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; } - .justify-around-l { -ms-flex-pack: distribute; justify-content: space-around; } - - .content-start-l { -ms-flex-line-pack: start; align-content: flex-start; } - .content-end-l { -ms-flex-line-pack: end; align-content: flex-end; } - .content-center-l { -ms-flex-line-pack: center; align-content: center; } - .content-between-l { -ms-flex-line-pack: justify; align-content: space-between; } - .content-around-l { -ms-flex-line-pack: distribute; align-content: space-around; } - .content-stretch-l { -ms-flex-line-pack: stretch; align-content: stretch; } - - .order-0-l { -webkit-box-ordinal-group: 1; -ms-flex-order: 0; order: 0; } - .order-1-l { -webkit-box-ordinal-group: 2; -ms-flex-order: 1; order: 1; } - .order-2-l { -webkit-box-ordinal-group: 3; -ms-flex-order: 2; order: 2; } - .order-3-l { -webkit-box-ordinal-group: 4; -ms-flex-order: 3; order: 3; } - .order-4-l { -webkit-box-ordinal-group: 5; -ms-flex-order: 4; order: 4; } - .order-5-l { -webkit-box-ordinal-group: 6; -ms-flex-order: 5; order: 5; } - .order-6-l { -webkit-box-ordinal-group: 7; -ms-flex-order: 6; order: 6; } - .order-7-l { -webkit-box-ordinal-group: 8; -ms-flex-order: 7; order: 7; } - .order-8-l { -webkit-box-ordinal-group: 9; -ms-flex-order: 8; order: 8; } - .order-last-l { -webkit-box-ordinal-group: 100000; -ms-flex-order: 99999; order: 99999; } - - .flex-grow-0-l { -webkit-box-flex: 0; -ms-flex-positive: 0; flex-grow: 0; } - .flex-grow-1-l { -webkit-box-flex: 1; -ms-flex-positive: 1; flex-grow: 1; } - - .flex-shrink-0-l { -ms-flex-negative: 0; flex-shrink: 0; } - .flex-shrink-1-l { -ms-flex-negative: 1; flex-shrink: 1; } -} -/* - - FLOATS - http://tachyons.io/docs/layout/floats/ - - 1. Floated elements are automatically rendered as block level elements. - Setting floats to display inline will fix the double margin bug in - ie6. You know... just in case. - - 2. Don't forget to clearfix your floats with .cf - - Base: - f = float - - Modifiers: - l = left - r = right - n = none - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.fl { float: left; _display: inline; } -.fr { float: right; _display: inline; } -.fn { float: none; } -@media screen and (min-width: 30em) { - .fl-ns { float: left; _display: inline; } - .fr-ns { float: right; _display: inline; } - .fn-ns { float: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .fl-m { float: left; _display: inline; } - .fr-m { float: right; _display: inline; } - .fn-m { float: none; } -} -@media screen and (min-width: 60em) { - .fl-l { float: left; _display: inline; } - .fr-l { float: right; _display: inline; } - .fn-l { float: none; } -} -/*@import 'tachyons/src/_font-family';*/ -/* - - FONT STYLE - Docs: http://tachyons.io/docs/typography/font-style/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.i { font-style: italic; } -.fs-normal { font-style: normal; } -@media screen and (min-width: 30em) { - .i-ns { font-style: italic; } - .fs-normal-ns { font-style: normal; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .i-m { font-style: italic; } - .fs-normal-m { font-style: normal; } -} -@media screen and (min-width: 60em) { - .i-l { font-style: italic; } - .fs-normal-l { font-style: normal; } -} -/* - - FONT WEIGHT - Docs: http://tachyons.io/docs/typography/font-weight/ - - Base - fw = font-weight - - Modifiers: - 1 = literal value 100 - 2 = literal value 200 - 3 = literal value 300 - 4 = literal value 400 - 5 = literal value 500 - 6 = literal value 600 - 7 = literal value 700 - 8 = literal value 800 - 9 = literal value 900 - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.normal { font-weight: normal; } -.b { font-weight: bold; } -.fw1 { font-weight: 100; } -.fw2 { font-weight: 200; } -.fw3 { font-weight: 300; } -.fw4 { font-weight: 400; } -.fw5 { font-weight: 500; } -.fw6 { font-weight: 600; } -.fw7 { font-weight: 700; } -.fw8 { font-weight: 800; } -.fw9 { font-weight: 900; } -@media screen and (min-width: 30em) { - .normal-ns { font-weight: normal; } - .b-ns { font-weight: bold; } - .fw1-ns { font-weight: 100; } - .fw2-ns { font-weight: 200; } - .fw3-ns { font-weight: 300; } - .fw4-ns { font-weight: 400; } - .fw5-ns { font-weight: 500; } - .fw6-ns { font-weight: 600; } - .fw7-ns { font-weight: 700; } - .fw8-ns { font-weight: 800; } - .fw9-ns { font-weight: 900; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .normal-m { font-weight: normal; } - .b-m { font-weight: bold; } - .fw1-m { font-weight: 100; } - .fw2-m { font-weight: 200; } - .fw3-m { font-weight: 300; } - .fw4-m { font-weight: 400; } - .fw5-m { font-weight: 500; } - .fw6-m { font-weight: 600; } - .fw7-m { font-weight: 700; } - .fw8-m { font-weight: 800; } - .fw9-m { font-weight: 900; } -} -@media screen and (min-width: 60em) { - .normal-l { font-weight: normal; } - .b-l { font-weight: bold; } - .fw1-l { font-weight: 100; } - .fw2-l { font-weight: 200; } - .fw3-l { font-weight: 300; } - .fw4-l { font-weight: 400; } - .fw5-l { font-weight: 500; } - .fw6-l { font-weight: 600; } - .fw7-l { font-weight: 700; } - .fw8-l { font-weight: 800; } - .fw9-l { font-weight: 900; } -} -/* - - FORMS - -*/ -.input-reset { - -webkit-appearance: none; - -moz-appearance: none; -} -.button-reset::-moz-focus-inner, -.input-reset::-moz-focus-inner { - border: 0; - padding: 0; -} -/* - - HEIGHTS - Docs: http://tachyons.io/docs/layout/heights/ - - Base: - h = height - min-h = min-height - min-vh = min-height vertical screen height - vh = vertical screen height - - Modifiers - 1 = 1st step in height scale - 2 = 2nd step in height scale - 3 = 3rd step in height scale - 4 = 4th step in height scale - 5 = 5th step in height scale - - -25 = literal value 25% - -50 = literal value 50% - -75 = literal value 75% - -100 = literal value 100% - - -auto = string value of auto - -inherit = string value of inherit - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* Height Scale */ -.h1 { height: 1rem; } -.h2 { height: 2rem; } -.h3 { height: 4rem; } -.h4 { height: 8rem; } -.h5 { height: 16rem; } -/* Height Percentages - Based off of height of parent */ -.h-25 { height: 25%; } -.h-50 { height: 50%; } -.h-75 { height: 75%; } -.h-100 { height: 100%; } -.min-h-100 { min-height: 100%; } -/* Screen Height Percentage */ -.vh-25 { height: 25vh; } -.vh-50 { height: 50vh; } -.vh-75 { height: 75vh; } -.vh-100 { height: 100vh; } -.min-vh-100 { min-height: 100vh; } -/* String Properties */ -.h-auto { height: auto; } -.h-inherit { height: inherit; } -@media screen and (min-width: 30em) { - .h1-ns { height: 1rem; } - .h2-ns { height: 2rem; } - .h3-ns { height: 4rem; } - .h4-ns { height: 8rem; } - .h5-ns { height: 16rem; } - .h-25-ns { height: 25%; } - .h-50-ns { height: 50%; } - .h-75-ns { height: 75%; } - .h-100-ns { height: 100%; } - .min-h-100-ns { min-height: 100%; } - .vh-25-ns { height: 25vh; } - .vh-50-ns { height: 50vh; } - .vh-75-ns { height: 75vh; } - .vh-100-ns { height: 100vh; } - .min-vh-100-ns { min-height: 100vh; } - .h-auto-ns { height: auto; } - .h-inherit-ns { height: inherit; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .h1-m { height: 1rem; } - .h2-m { height: 2rem; } - .h3-m { height: 4rem; } - .h4-m { height: 8rem; } - .h5-m { height: 16rem; } - .h-25-m { height: 25%; } - .h-50-m { height: 50%; } - .h-75-m { height: 75%; } - .h-100-m { height: 100%; } - .min-h-100-m { min-height: 100%; } - .vh-25-m { height: 25vh; } - .vh-50-m { height: 50vh; } - .vh-75-m { height: 75vh; } - .vh-100-m { height: 100vh; } - .min-vh-100-m { min-height: 100vh; } - .h-auto-m { height: auto; } - .h-inherit-m { height: inherit; } -} -@media screen and (min-width: 60em) { - .h1-l { height: 1rem; } - .h2-l { height: 2rem; } - .h3-l { height: 4rem; } - .h4-l { height: 8rem; } - .h5-l { height: 16rem; } - .h-25-l { height: 25%; } - .h-50-l { height: 50%; } - .h-75-l { height: 75%; } - .h-100-l { height: 100%; } - .min-h-100-l { min-height: 100%; } - .vh-25-l { height: 25vh; } - .vh-50-l { height: 50vh; } - .vh-75-l { height: 75vh; } - .vh-100-l { height: 100vh; } - .min-vh-100-l { min-height: 100vh; } - .h-auto-l { height: auto; } - .h-inherit-l { height: inherit; } -} -/* - - LETTER SPACING - Docs: http://tachyons.io/docs/typography/tracking/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.tracked { letter-spacing: .1em; } -.tracked-tight { letter-spacing: -.05em; } -.tracked-mega { letter-spacing: .25em; } -@media screen and (min-width: 30em) { - .tracked-ns { letter-spacing: .1em; } - .tracked-tight-ns { letter-spacing: -.05em; } - .tracked-mega-ns { letter-spacing: .25em; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .tracked-m { letter-spacing: .1em; } - .tracked-tight-m { letter-spacing: -.05em; } - .tracked-mega-m { letter-spacing: .25em; } -} -@media screen and (min-width: 60em) { - .tracked-l { letter-spacing: .1em; } - .tracked-tight-l { letter-spacing: -.05em; } - .tracked-mega-l { letter-spacing: .25em; } -} -/* - - LINE HEIGHT / LEADING - Docs: http://tachyons.io/docs/typography/line-height - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.lh-solid { line-height: 1; } -.lh-title { line-height: 1.25; } -.lh-copy { line-height: 1.5; } -@media screen and (min-width: 30em) { - .lh-solid-ns { line-height: 1; } - .lh-title-ns { line-height: 1.25; } - .lh-copy-ns { line-height: 1.5; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .lh-solid-m { line-height: 1; } - .lh-title-m { line-height: 1.25; } - .lh-copy-m { line-height: 1.5; } -} -@media screen and (min-width: 60em) { - .lh-solid-l { line-height: 1; } - .lh-title-l { line-height: 1.25; } - .lh-copy-l { line-height: 1.5; } -} -/* - - LINKS - Docs: http://tachyons.io/docs/elements/links/ - -*/ -.link { - text-decoration: none; - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -.link:link, -.link:visited { - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -.link:hover { - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -.link:active { - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -.link:focus { - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; - outline: 1px dotted currentColor; -} -/* - - LISTS - http://tachyons.io/docs/elements/lists/ - -*/ -.list { list-style-type: none; } -/* - - MAX WIDTHS - Docs: http://tachyons.io/docs/layout/max-widths/ - - Base: - mw = max-width - - Modifiers - 1 = 1st step in width scale - 2 = 2nd step in width scale - 3 = 3rd step in width scale - 4 = 4th step in width scale - 5 = 5th step in width scale - 6 = 6st step in width scale - 7 = 7nd step in width scale - 8 = 8rd step in width scale - 9 = 9th step in width scale - - -100 = literal value 100% - - -none = string value none - - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* Max Width Percentages */ -.mw-100 { max-width: 100%; } -/* Max Width Scale */ -.mw1 { max-width: 1rem; } -.mw2 { max-width: 2rem; } -.mw3 { max-width: 4rem; } -.mw4 { max-width: 8rem; } -.mw5 { max-width: 16rem; } -.mw6 { max-width: 32rem; } -.mw7 { max-width: 48rem; } -.mw8 { max-width: 64rem; } -.mw9 { max-width: 96rem; } -/* Max Width String Properties */ -.mw-none { max-width: none; } -@media screen and (min-width: 30em) { - .mw-100-ns { max-width: 100%; } - - .mw1-ns { max-width: 1rem; } - .mw2-ns { max-width: 2rem; } - .mw3-ns { max-width: 4rem; } - .mw4-ns { max-width: 8rem; } - .mw5-ns { max-width: 16rem; } - .mw6-ns { max-width: 32rem; } - .mw7-ns { max-width: 48rem; } - .mw8-ns { max-width: 64rem; } - .mw9-ns { max-width: 96rem; } - - .mw-none-ns { max-width: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .mw-100-m { max-width: 100%; } - - .mw1-m { max-width: 1rem; } - .mw2-m { max-width: 2rem; } - .mw3-m { max-width: 4rem; } - .mw4-m { max-width: 8rem; } - .mw5-m { max-width: 16rem; } - .mw6-m { max-width: 32rem; } - .mw7-m { max-width: 48rem; } - .mw8-m { max-width: 64rem; } - .mw9-m { max-width: 96rem; } - - .mw-none-m { max-width: none; } -} -@media screen and (min-width: 60em) { - .mw-100-l { max-width: 100%; } - - .mw1-l { max-width: 1rem; } - .mw2-l { max-width: 2rem; } - .mw3-l { max-width: 4rem; } - .mw4-l { max-width: 8rem; } - .mw5-l { max-width: 16rem; } - .mw6-l { max-width: 32rem; } - .mw7-l { max-width: 48rem; } - .mw8-l { max-width: 64rem; } - .mw9-l { max-width: 96rem; } - - .mw-none-l { max-width: none; } -} -/* - - WIDTHS - Docs: http://tachyons.io/docs/layout/widths/ - - Base: - w = width - - Modifiers - 1 = 1st step in width scale - 2 = 2nd step in width scale - 3 = 3rd step in width scale - 4 = 4th step in width scale - 5 = 5th step in width scale - - -10 = literal value 10% - -20 = literal value 20% - -25 = literal value 25% - -30 = literal value 30% - -33 = literal value 33% - -34 = literal value 34% - -40 = literal value 40% - -50 = literal value 50% - -60 = literal value 60% - -70 = literal value 70% - -75 = literal value 75% - -80 = literal value 80% - -90 = literal value 90% - -100 = literal value 100% - - -third = 100% / 3 (Not supported in opera mini or IE8) - -two-thirds = 100% / 1.5 (Not supported in opera mini or IE8) - -auto = string value auto - - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* Width Scale */ -.w1 { width: 1rem; } -.w2 { width: 2rem; } -.w3 { width: 4rem; } -.w4 { width: 8rem; } -.w5 { width: 16rem; } -.w-10 { width: 10%; } -.w-20 { width: 20%; } -.w-25 { width: 25%; } -.w-30 { width: 30%; } -.w-33 { width: 33%; } -.w-34 { width: 34%; } -.w-40 { width: 40%; } -.w-50 { width: 50%; } -.w-60 { width: 60%; } -.w-70 { width: 70%; } -.w-75 { width: 75%; } -.w-80 { width: 80%; } -.w-90 { width: 90%; } -.w-100 { width: 100%; } -.w-third { width: 33.33333%; } -.w-two-thirds { width: 66.66667%; } -.w-auto { width: auto; } -@media screen and (min-width: 30em) { - .w1-ns { width: 1rem; } - .w2-ns { width: 2rem; } - .w3-ns { width: 4rem; } - .w4-ns { width: 8rem; } - .w5-ns { width: 16rem; } - .w-10-ns { width: 10%; } - .w-20-ns { width: 20%; } - .w-25-ns { width: 25%; } - .w-30-ns { width: 30%; } - .w-33-ns { width: 33%; } - .w-34-ns { width: 34%; } - .w-40-ns { width: 40%; } - .w-50-ns { width: 50%; } - .w-60-ns { width: 60%; } - .w-70-ns { width: 70%; } - .w-75-ns { width: 75%; } - .w-80-ns { width: 80%; } - .w-90-ns { width: 90%; } - .w-100-ns { width: 100%; } - .w-third-ns { width: 33.33333%; } - .w-two-thirds-ns { width: 66.66667%; } - .w-auto-ns { width: auto; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .w1-m { width: 1rem; } - .w2-m { width: 2rem; } - .w3-m { width: 4rem; } - .w4-m { width: 8rem; } - .w5-m { width: 16rem; } - .w-10-m { width: 10%; } - .w-20-m { width: 20%; } - .w-25-m { width: 25%; } - .w-30-m { width: 30%; } - .w-33-m { width: 33%; } - .w-34-m { width: 34%; } - .w-40-m { width: 40%; } - .w-50-m { width: 50%; } - .w-60-m { width: 60%; } - .w-70-m { width: 70%; } - .w-75-m { width: 75%; } - .w-80-m { width: 80%; } - .w-90-m { width: 90%; } - .w-100-m { width: 100%; } - .w-third-m { width: 33.33333%; } - .w-two-thirds-m { width: 66.66667%; } - .w-auto-m { width: auto; } -} -@media screen and (min-width: 60em) { - .w1-l { width: 1rem; } - .w2-l { width: 2rem; } - .w3-l { width: 4rem; } - .w4-l { width: 8rem; } - .w5-l { width: 16rem; } - .w-10-l { width: 10%; } - .w-20-l { width: 20%; } - .w-25-l { width: 25%; } - .w-30-l { width: 30%; } - .w-33-l { width: 33%; } - .w-34-l { width: 34%; } - .w-40-l { width: 40%; } - .w-50-l { width: 50%; } - .w-60-l { width: 60%; } - .w-70-l { width: 70%; } - .w-75-l { width: 75%; } - .w-80-l { width: 80%; } - .w-90-l { width: 90%; } - .w-100-l { width: 100%; } - .w-third-l { width: 33.33333%; } - .w-two-thirds-l { width: 66.66667%; } - .w-auto-l { width: auto; } -} -/* - - OVERFLOW - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - - */ -.overflow-visible { overflow: visible; } -.overflow-hidden { overflow: hidden; } -.overflow-scroll { overflow: scroll; } -.overflow-auto { overflow: auto; } -.overflow-x-visible { overflow-x: visible; } -.overflow-x-hidden { overflow-x: hidden; } -.overflow-x-scroll { overflow-x: scroll; } -.overflow-x-auto { overflow-x: auto; } -.overflow-y-visible { overflow-y: visible; } -.overflow-y-hidden { overflow-y: hidden; } -.overflow-y-scroll { overflow-y: scroll; } -.overflow-y-auto { overflow-y: auto; } -@media screen and (min-width: 30em) { - .overflow-visible-ns { overflow: visible; } - .overflow-hidden-ns { overflow: hidden; } - .overflow-scroll-ns { overflow: scroll; } - .overflow-auto-ns { overflow: auto; } - .overflow-x-visible-ns { overflow-x: visible; } - .overflow-x-hidden-ns { overflow-x: hidden; } - .overflow-x-scroll-ns { overflow-x: scroll; } - .overflow-x-auto-ns { overflow-x: auto; } - - .overflow-y-visible-ns { overflow-y: visible; } - .overflow-y-hidden-ns { overflow-y: hidden; } - .overflow-y-scroll-ns { overflow-y: scroll; } - .overflow-y-auto-ns { overflow-y: auto; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .overflow-visible-m { overflow: visible; } - .overflow-hidden-m { overflow: hidden; } - .overflow-scroll-m { overflow: scroll; } - .overflow-auto-m { overflow: auto; } - - .overflow-x-visible-m { overflow-x: visible; } - .overflow-x-hidden-m { overflow-x: hidden; } - .overflow-x-scroll-m { overflow-x: scroll; } - .overflow-x-auto-m { overflow-x: auto; } - - .overflow-y-visible-m { overflow-y: visible; } - .overflow-y-hidden-m { overflow-y: hidden; } - .overflow-y-scroll-m { overflow-y: scroll; } - .overflow-y-auto-m { overflow-y: auto; } -} -@media screen and (min-width: 60em) { - .overflow-visible-l { overflow: visible; } - .overflow-hidden-l { overflow: hidden; } - .overflow-scroll-l { overflow: scroll; } - .overflow-auto-l { overflow: auto; } - - .overflow-x-visible-l { overflow-x: visible; } - .overflow-x-hidden-l { overflow-x: hidden; } - .overflow-x-scroll-l { overflow-x: scroll; } - .overflow-x-auto-l { overflow-x: auto; } - - .overflow-y-visible-l { overflow-y: visible; } - .overflow-y-hidden-l { overflow-y: hidden; } - .overflow-y-scroll-l { overflow-y: scroll; } - .overflow-y-auto-l { overflow-y: auto; } -} -/* - - POSITIONING - Docs: http://tachyons.io/docs/layout/position/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.static { position: static; } -.relative { position: relative; } -.absolute { position: absolute; } -.fixed { position: fixed; } -@media screen and (min-width: 30em) { - .static-ns { position: static; } - .relative-ns { position: relative; } - .absolute-ns { position: absolute; } - .fixed-ns { position: fixed; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .static-m { position: static; } - .relative-m { position: relative; } - .absolute-m { position: absolute; } - .fixed-m { position: fixed; } -} -@media screen and (min-width: 60em) { - .static-l { position: static; } - .relative-l { position: relative; } - .absolute-l { position: absolute; } - .fixed-l { position: fixed; } -} -/* - - OPACITY - Docs: http://tachyons.io/docs/themes/opacity/ - -*/ -.o-100 { opacity: 1; } -.o-90 { opacity: .9; } -.o-80 { opacity: .8; } -.o-70 { opacity: .7; } -.o-60 { opacity: .6; } -.o-50 { opacity: .5; } -.o-40 { opacity: .4; } -.o-30 { opacity: .3; } -.o-20 { opacity: .2; } -.o-10 { opacity: .1; } -.o-05 { opacity: .05; } -.o-025 { opacity: .025; } -.o-0 { opacity: 0; } -/*@import 'tachyons/src/_rotations';*/ -/* - - SKINS - Docs: http://tachyons.io/docs/themes/skins/ - - Classes for setting foreground and background colors on elements. - If you haven't declared a border color, but set border on an element, it will - be set to the current text color. - -*/ -/* Text colors */ -.black-90 { color: rgba(0, 0, 0, .9); } -.black-80 { color: rgba(0, 0, 0, .8); } -.black-70 { color: rgba(0, 0, 0, .7); } -.black-60 { color: rgba(0, 0, 0, .6); } -.black-50 { color: rgba(0, 0, 0, .5); } -.black-40 { color: rgba(0, 0, 0, .4); } -.black-30 { color: rgba(0, 0, 0, .3); } -.black-20 { color: rgba(0, 0, 0, .2); } -.black-10 { color: rgba(0, 0, 0, .1); } -.black-05 { color: rgba(0, 0, 0, .05); } -.white-90 { color: rgba(255, 255, 255, .9); } -.white-80 { color: rgba(255, 255, 255, .8); } -.white-70 { color: rgba(255, 255, 255, .7); } -.white-60 { color: rgba(255, 255, 255, .6); } -.white-50 { color: rgba(255, 255, 255, .5); } -.white-40 { color: rgba(255, 255, 255, .4); } -.white-30 { color: rgba(255, 255, 255, .3); } -.white-20 { color: rgba(255, 255, 255, .2); } -.white-10 { color: rgba(255, 255, 255, .1); } -.black { color: #000; } -.near-black { color: #111; } -.dark-gray { color: #333; } -.mid-gray { color: #555; } -.gray { color: #777; } -.silver { color: #999; } -.light-silver { color: #aaa; } -.moon-gray { color: #ccc; } -.light-gray { color: #eee; } -.near-white { color: #f4f4f4; } -.white { color: #fff; } -.dark-red { color: #e7040f; } -.red { color: #ff4136; } -.light-red { color: #ff725c; } -.orange { color: #ff6300; } -.gold { color: #ffb700; } -.yellow { color: #ffd700; } -.light-yellow { color: #fbf1a9; } -.purple { color: #5e2ca5; } -.light-purple { color: #a463f2; } -.dark-pink { color: #d5008f; } -.hot-pink { color: #ff41b4; } -.pink { color: #ff80cc; } -.light-pink { color: #ffa3d7; } -.dark-green { color: #137752; } -.green { color: #19a974; } -.light-green { color: #9eebcf; } -.navy { color: #001b44; } -.dark-blue { color: #00449e; } -.blue { color: #0594CB; } -.light-blue { color: #96ccff; } -.lightest-blue { color: #cdecff; } -.washed-blue { color: #f6fffe; } -.washed-green { color: #e8fdf5; } -.washed-yellow { color: #fffceb; } -.washed-red { color: #ffdfdf; } -.color-inherit { color: inherit; } -.bg-black-90 { background-color: rgba(0, 0, 0, .9); } -.bg-black-80 { background-color: rgba(0, 0, 0, .8); } -.bg-black-70 { background-color: rgba(0, 0, 0, .7); } -.bg-black-60 { background-color: rgba(0, 0, 0, .6); } -.bg-black-50 { background-color: rgba(0, 0, 0, .5); } -.bg-black-40 { background-color: rgba(0, 0, 0, .4); } -.bg-black-30 { background-color: rgba(0, 0, 0, .3); } -.bg-black-20 { background-color: rgba(0, 0, 0, .2); } -.bg-black-10 { background-color: rgba(0, 0, 0, .1); } -.bg-black-05 { background-color: rgba(0, 0, 0, .05); } -.bg-white-90 { background-color: rgba(255, 255, 255, .9); } -.bg-white-80 { background-color: rgba(255, 255, 255, .8); } -.bg-white-70 { background-color: rgba(255, 255, 255, .7); } -.bg-white-60 { background-color: rgba(255, 255, 255, .6); } -.bg-white-50 { background-color: rgba(255, 255, 255, .5); } -.bg-white-40 { background-color: rgba(255, 255, 255, .4); } -.bg-white-30 { background-color: rgba(255, 255, 255, .3); } -.bg-white-20 { background-color: rgba(255, 255, 255, .2); } -.bg-white-10 { background-color: rgba(255, 255, 255, .1); } -/* Background colors */ -.bg-black { background-color: #000; } -.bg-near-black { background-color: #111; } -.bg-dark-gray { background-color: #333; } -.bg-mid-gray { background-color: #555; } -.bg-gray { background-color: #777; } -.bg-silver { background-color: #999; } -.bg-light-silver { background-color: #aaa; } -.bg-moon-gray { background-color: #ccc; } -.bg-light-gray { background-color: #eee; } -.bg-near-white { background-color: #f4f4f4; } -.bg-white { background-color: #fff; } -.bg-transparent { background-color: transparent; } -.bg-dark-red { background-color: #e7040f; } -.bg-red { background-color: #ff4136; } -.bg-light-red { background-color: #ff725c; } -.bg-orange { background-color: #ff6300; } -.bg-gold { background-color: #ffb700; } -.bg-yellow { background-color: #ffd700; } -.bg-light-yellow { background-color: #fbf1a9; } -.bg-purple { background-color: #5e2ca5; } -.bg-light-purple { background-color: #a463f2; } -.bg-dark-pink { background-color: #d5008f; } -.bg-hot-pink { background-color: #ff41b4; } -.bg-pink { background-color: #ff80cc; } -.bg-light-pink { background-color: #ffa3d7; } -.bg-dark-green { background-color: #137752; } -.bg-green { background-color: #19a974; } -.bg-light-green { background-color: #9eebcf; } -.bg-navy { background-color: #001b44; } -.bg-dark-blue { background-color: #00449e; } -.bg-blue { background-color: #0594CB; } -.bg-light-blue { background-color: #96ccff; } -.bg-lightest-blue { background-color: #cdecff; } -.bg-washed-blue { background-color: #f6fffe; } -.bg-washed-green { background-color: #e8fdf5; } -.bg-washed-yellow { background-color: #fffceb; } -.bg-washed-red { background-color: #ffdfdf; } -.bg-inherit { background-color: inherit; } -/* - - SKINS:PSEUDO - - Customize the color of an element when - it is focused or hovered over. - - */ -.hover-black:hover, -.hover-black:focus { color: #000; } -.hover-near-black:hover, -.hover-near-black:focus { color: #111; } -.hover-dark-gray:hover, -.hover-dark-gray:focus { color: #333; } -.hover-mid-gray:hover, -.hover-mid-gray:focus { color: #555; } -.hover-gray:hover, -.hover-gray:focus { color: #777; } -.hover-silver:hover, -.hover-silver:focus { color: #999; } -.hover-light-silver:hover, -.hover-light-silver:focus { color: #aaa; } -.hover-moon-gray:hover, -.hover-moon-gray:focus { color: #ccc; } -.hover-light-gray:hover, -.hover-light-gray:focus { color: #eee; } -.hover-near-white:hover, -.hover-near-white:focus { color: #f4f4f4; } -.hover-white:hover, -.hover-white:focus { color: #fff; } -.hover-black-90:hover, -.hover-black-90:focus { color: rgba(0, 0, 0, .9); } -.hover-black-80:hover, -.hover-black-80:focus { color: rgba(0, 0, 0, .8); } -.hover-black-70:hover, -.hover-black-70:focus { color: rgba(0, 0, 0, .7); } -.hover-black-60:hover, -.hover-black-60:focus { color: rgba(0, 0, 0, .6); } -.hover-black-50:hover, -.hover-black-50:focus { color: rgba(0, 0, 0, .5); } -.hover-black-40:hover, -.hover-black-40:focus { color: rgba(0, 0, 0, .4); } -.hover-black-30:hover, -.hover-black-30:focus { color: rgba(0, 0, 0, .3); } -.hover-black-20:hover, -.hover-black-20:focus { color: rgba(0, 0, 0, .2); } -.hover-black-10:hover, -.hover-black-10:focus { color: rgba(0, 0, 0, .1); } -.hover-white-90:hover, -.hover-white-90:focus { color: rgba(255, 255, 255, .9); } -.hover-white-80:hover, -.hover-white-80:focus { color: rgba(255, 255, 255, .8); } -.hover-white-70:hover, -.hover-white-70:focus { color: rgba(255, 255, 255, .7); } -.hover-white-60:hover, -.hover-white-60:focus { color: rgba(255, 255, 255, .6); } -.hover-white-50:hover, -.hover-white-50:focus { color: rgba(255, 255, 255, .5); } -.hover-white-40:hover, -.hover-white-40:focus { color: rgba(255, 255, 255, .4); } -.hover-white-30:hover, -.hover-white-30:focus { color: rgba(255, 255, 255, .3); } -.hover-white-20:hover, -.hover-white-20:focus { color: rgba(255, 255, 255, .2); } -.hover-white-10:hover, -.hover-white-10:focus { color: rgba(255, 255, 255, .1); } -.hover-inherit:hover, -.hover-inherit:focus { color: inherit; } -.hover-bg-black:hover, -.hover-bg-black:focus { background-color: #000; } -.hover-bg-near-black:hover, -.hover-bg-near-black:focus { background-color: #111; } -.hover-bg-dark-gray:hover, -.hover-bg-dark-gray:focus { background-color: #333; } -.hover-bg-mid-gray:hover, -.hover-bg-mid-gray:focus { background-color: #555; } -.hover-bg-gray:hover, -.hover-bg-gray:focus { background-color: #777; } -.hover-bg-silver:hover, -.hover-bg-silver:focus { background-color: #999; } -.hover-bg-light-silver:hover, -.hover-bg-light-silver:focus { background-color: #aaa; } -.hover-bg-moon-gray:hover, -.hover-bg-moon-gray:focus { background-color: #ccc; } -.hover-bg-light-gray:hover, -.hover-bg-light-gray:focus { background-color: #eee; } -.hover-bg-near-white:hover, -.hover-bg-near-white:focus { background-color: #f4f4f4; } -.hover-bg-white:hover, -.hover-bg-white:focus { background-color: #fff; } -.hover-bg-transparent:hover, -.hover-bg-transparent:focus { background-color: transparent; } -.hover-bg-black-90:hover, -.hover-bg-black-90:focus { background-color: rgba(0, 0, 0, .9); } -.hover-bg-black-80:hover, -.hover-bg-black-80:focus { background-color: rgba(0, 0, 0, .8); } -.hover-bg-black-70:hover, -.hover-bg-black-70:focus { background-color: rgba(0, 0, 0, .7); } -.hover-bg-black-60:hover, -.hover-bg-black-60:focus { background-color: rgba(0, 0, 0, .6); } -.hover-bg-black-50:hover, -.hover-bg-black-50:focus { background-color: rgba(0, 0, 0, .5); } -.hover-bg-black-40:hover, -.hover-bg-black-40:focus { background-color: rgba(0, 0, 0, .4); } -.hover-bg-black-30:hover, -.hover-bg-black-30:focus { background-color: rgba(0, 0, 0, .3); } -.hover-bg-black-20:hover, -.hover-bg-black-20:focus { background-color: rgba(0, 0, 0, .2); } -.hover-bg-black-10:hover, -.hover-bg-black-10:focus { background-color: rgba(0, 0, 0, .1); } -.hover-bg-white-90:hover, -.hover-bg-white-90:focus { background-color: rgba(255, 255, 255, .9); } -.hover-bg-white-80:hover, -.hover-bg-white-80:focus { background-color: rgba(255, 255, 255, .8); } -.hover-bg-white-70:hover, -.hover-bg-white-70:focus { background-color: rgba(255, 255, 255, .7); } -.hover-bg-white-60:hover, -.hover-bg-white-60:focus { background-color: rgba(255, 255, 255, .6); } -.hover-bg-white-50:hover, -.hover-bg-white-50:focus { background-color: rgba(255, 255, 255, .5); } -.hover-bg-white-40:hover, -.hover-bg-white-40:focus { background-color: rgba(255, 255, 255, .4); } -.hover-bg-white-30:hover, -.hover-bg-white-30:focus { background-color: rgba(255, 255, 255, .3); } -.hover-bg-white-20:hover, -.hover-bg-white-20:focus { background-color: rgba(255, 255, 255, .2); } -.hover-bg-white-10:hover, -.hover-bg-white-10:focus { background-color: rgba(255, 255, 255, .1); } -.hover-dark-red:hover, -.hover-dark-red:focus { color: #e7040f; } -.hover-red:hover, -.hover-red:focus { color: #ff4136; } -.hover-light-red:hover, -.hover-light-red:focus { color: #ff725c; } -.hover-orange:hover, -.hover-orange:focus { color: #ff6300; } -.hover-gold:hover, -.hover-gold:focus { color: #ffb700; } -.hover-yellow:hover, -.hover-yellow:focus { color: #ffd700; } -.hover-light-yellow:hover, -.hover-light-yellow:focus { color: #fbf1a9; } -.hover-purple:hover, -.hover-purple:focus { color: #5e2ca5; } -.hover-light-purple:hover, -.hover-light-purple:focus { color: #a463f2; } -.hover-dark-pink:hover, -.hover-dark-pink:focus { color: #d5008f; } -.hover-hot-pink:hover, -.hover-hot-pink:focus { color: #ff41b4; } -.hover-pink:hover, -.hover-pink:focus { color: #ff80cc; } -.hover-light-pink:hover, -.hover-light-pink:focus { color: #ffa3d7; } -.hover-dark-green:hover, -.hover-dark-green:focus { color: #137752; } -.hover-green:hover, -.hover-green:focus { color: #19a974; } -.hover-light-green:hover, -.hover-light-green:focus { color: #9eebcf; } -.hover-navy:hover, -.hover-navy:focus { color: #001b44; } -.hover-dark-blue:hover, -.hover-dark-blue:focus { color: #00449e; } -.hover-blue:hover, -.hover-blue:focus { color: #0594CB; } -.hover-light-blue:hover, -.hover-light-blue:focus { color: #96ccff; } -.hover-lightest-blue:hover, -.hover-lightest-blue:focus { color: #cdecff; } -.hover-washed-blue:hover, -.hover-washed-blue:focus { color: #f6fffe; } -.hover-washed-green:hover, -.hover-washed-green:focus { color: #e8fdf5; } -.hover-washed-yellow:hover, -.hover-washed-yellow:focus { color: #fffceb; } -.hover-washed-red:hover, -.hover-washed-red:focus { color: #ffdfdf; } -.hover-bg-dark-red:hover, -.hover-bg-dark-red:focus { background-color: #e7040f; } -.hover-bg-red:hover, -.hover-bg-red:focus { background-color: #ff4136; } -.hover-bg-light-red:hover, -.hover-bg-light-red:focus { background-color: #ff725c; } -.hover-bg-orange:hover, -.hover-bg-orange:focus { background-color: #ff6300; } -.hover-bg-gold:hover, -.hover-bg-gold:focus { background-color: #ffb700; } -.hover-bg-yellow:hover, -.hover-bg-yellow:focus { background-color: #ffd700; } -.hover-bg-light-yellow:hover, -.hover-bg-light-yellow:focus { background-color: #fbf1a9; } -.hover-bg-purple:hover, -.hover-bg-purple:focus { background-color: #5e2ca5; } -.hover-bg-light-purple:hover, -.hover-bg-light-purple:focus { background-color: #a463f2; } -.hover-bg-dark-pink:hover, -.hover-bg-dark-pink:focus { background-color: #d5008f; } -.hover-bg-hot-pink:hover, -.hover-bg-hot-pink:focus { background-color: #ff41b4; } -.hover-bg-pink:hover, -.hover-bg-pink:focus { background-color: #ff80cc; } -.hover-bg-light-pink:hover, -.hover-bg-light-pink:focus { background-color: #ffa3d7; } -.hover-bg-dark-green:hover, -.hover-bg-dark-green:focus { background-color: #137752; } -.hover-bg-green:hover, -.hover-bg-green:focus { background-color: #19a974; } -.hover-bg-light-green:hover, -.hover-bg-light-green:focus { background-color: #9eebcf; } -.hover-bg-navy:hover, -.hover-bg-navy:focus { background-color: #001b44; } -.hover-bg-dark-blue:hover, -.hover-bg-dark-blue:focus { background-color: #00449e; } -.hover-bg-blue:hover, -.hover-bg-blue:focus { background-color: #0594CB; } -.hover-bg-light-blue:hover, -.hover-bg-light-blue:focus { background-color: #96ccff; } -.hover-bg-lightest-blue:hover, -.hover-bg-lightest-blue:focus { background-color: #cdecff; } -.hover-bg-washed-blue:hover, -.hover-bg-washed-blue:focus { background-color: #f6fffe; } -.hover-bg-washed-green:hover, -.hover-bg-washed-green:focus { background-color: #e8fdf5; } -.hover-bg-washed-yellow:hover, -.hover-bg-washed-yellow:focus { background-color: #fffceb; } -.hover-bg-washed-red:hover, -.hover-bg-washed-red:focus { background-color: #ffdfdf; } -.hover-bg-inherit:hover, -.hover-bg-inherit:focus { background-color: inherit; } -/* Variables */ -/* - SPACING - Docs: http://tachyons.io/docs/layout/spacing/ - - An eight step powers of two scale ranging from 0 to 16rem. - - Base: - p = padding - m = margin - - Modifiers: - a = all - h = horizontal - v = vertical - t = top - r = right - b = bottom - l = left - - 0 = none - 1 = 1st step in spacing scale - 2 = 2nd step in spacing scale - 3 = 3rd step in spacing scale - 4 = 4th step in spacing scale - 5 = 5th step in spacing scale - 6 = 6th step in spacing scale - 7 = 7th step in spacing scale - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.pa0 { padding: 0; } -.pa1 { padding: .25rem; } -.pa2 { padding: .5rem; } -.pa3 { padding: 1rem; } -.pa4 { padding: 2rem; } -.pa5 { padding: 4rem; } -.pa6 { padding: 8rem; } -.pa7 { padding: 16rem; } -.pl0 { padding-left: 0; } -.pl1 { padding-left: .25rem; } -.pl2 { padding-left: .5rem; } -.pl3 { padding-left: 1rem; } -.pl4 { padding-left: 2rem; } -.pl5 { padding-left: 4rem; } -.pl6 { padding-left: 8rem; } -.pl7 { padding-left: 16rem; } -.pr0 { padding-right: 0; } -.pr1 { padding-right: .25rem; } -.pr2 { padding-right: .5rem; } -.pr3 { padding-right: 1rem; } -.pr4 { padding-right: 2rem; } -.pr5 { padding-right: 4rem; } -.pr6 { padding-right: 8rem; } -.pr7 { padding-right: 16rem; } -.pb0 { padding-bottom: 0; } -.pb1 { padding-bottom: .25rem; } -.pb2 { padding-bottom: .5rem; } -.pb3 { padding-bottom: 1rem; } -.pb4 { padding-bottom: 2rem; } -.pb5 { padding-bottom: 4rem; } -.pb6 { padding-bottom: 8rem; } -.pb7 { padding-bottom: 16rem; } -.pt0 { padding-top: 0; } -.pt1 { padding-top: .25rem; } -.pt2 { padding-top: .5rem; } -.pt3 { padding-top: 1rem; } -.pt4 { padding-top: 2rem; } -.pt5 { padding-top: 4rem; } -.pt6 { padding-top: 8rem; } -.pt7 { padding-top: 16rem; } -.pv0 { - padding-top: 0; - padding-bottom: 0; -} -.pv1 { - padding-top: .25rem; - padding-bottom: .25rem; -} -.pv2 { - padding-top: .5rem; - padding-bottom: .5rem; -} -.pv3 { - padding-top: 1rem; - padding-bottom: 1rem; -} -.pv4 { - padding-top: 2rem; - padding-bottom: 2rem; -} -.pv5 { - padding-top: 4rem; - padding-bottom: 4rem; -} -.pv6 { - padding-top: 8rem; - padding-bottom: 8rem; -} -.pv7 { - padding-top: 16rem; - padding-bottom: 16rem; -} -.ph0 { - padding-left: 0; - padding-right: 0; -} -.ph1 { - padding-left: .25rem; - padding-right: .25rem; -} -.ph2 { - padding-left: .5rem; - padding-right: .5rem; -} -.ph3 { - padding-left: 1rem; - padding-right: 1rem; -} -.ph4 { - padding-left: 2rem; - padding-right: 2rem; -} -.ph5 { - padding-left: 4rem; - padding-right: 4rem; -} -.ph6 { - padding-left: 8rem; - padding-right: 8rem; -} -.ph7 { - padding-left: 16rem; - padding-right: 16rem; -} -.ma0 { margin: 0; } -.ma1 { margin: .25rem; } -.ma2 { margin: .5rem; } -.ma3 { margin: 1rem; } -.ma4 { margin: 2rem; } -.ma5 { margin: 4rem; } -.ma6 { margin: 8rem; } -.ma7 { margin: 16rem; } -.ml0 { margin-left: 0; } -.ml1 { margin-left: .25rem; } -.ml2 { margin-left: .5rem; } -.ml3 { margin-left: 1rem; } -.ml4 { margin-left: 2rem; } -.ml5 { margin-left: 4rem; } -.ml6 { margin-left: 8rem; } -.ml7 { margin-left: 16rem; } -.mr0 { margin-right: 0; } -.mr1 { margin-right: .25rem; } -.mr2 { margin-right: .5rem; } -.mr3 { margin-right: 1rem; } -.mr4 { margin-right: 2rem; } -.mr5 { margin-right: 4rem; } -.mr6 { margin-right: 8rem; } -.mr7 { margin-right: 16rem; } -.mb0 { margin-bottom: 0; } -.mb1 { margin-bottom: .25rem; } -.mb2 { margin-bottom: .5rem; } -.mb3 { margin-bottom: 1rem; } -.mb4 { margin-bottom: 2rem; } -.mb5 { margin-bottom: 4rem; } -.mb6 { margin-bottom: 8rem; } -.mb7 { margin-bottom: 16rem; } -.mt0 { margin-top: 0; } -.mt1 { margin-top: .25rem; } -.mt2 { margin-top: .5rem; } -.mt3 { margin-top: 1rem; } -.mt4 { margin-top: 2rem; } -.mt5 { margin-top: 4rem; } -.mt6 { margin-top: 8rem; } -.mt7 { margin-top: 16rem; } -.mv0 { - margin-top: 0; - margin-bottom: 0; -} -.mv1 { - margin-top: .25rem; - margin-bottom: .25rem; -} -.mv2 { - margin-top: .5rem; - margin-bottom: .5rem; -} -.mv3 { - margin-top: 1rem; - margin-bottom: 1rem; -} -.mv4 { - margin-top: 2rem; - margin-bottom: 2rem; -} -.mv5 { - margin-top: 4rem; - margin-bottom: 4rem; -} -.mv6 { - margin-top: 8rem; - margin-bottom: 8rem; -} -.mv7 { - margin-top: 16rem; - margin-bottom: 16rem; -} -.mh0 { - margin-left: 0; - margin-right: 0; -} -.mh1 { - margin-left: .25rem; - margin-right: .25rem; -} -.mh2 { - margin-left: .5rem; - margin-right: .5rem; -} -.mh3 { - margin-left: 1rem; - margin-right: 1rem; -} -.mh4 { - margin-left: 2rem; - margin-right: 2rem; -} -.mh5 { - margin-left: 4rem; - margin-right: 4rem; -} -.mh6 { - margin-left: 8rem; - margin-right: 8rem; -} -.mh7 { - margin-left: 16rem; - margin-right: 16rem; -} -@media screen and (min-width: 30em) { - .pa0-ns { padding: 0; } - .pa1-ns { padding: .25rem; } - .pa2-ns { padding: .5rem; } - .pa3-ns { padding: 1rem; } - .pa4-ns { padding: 2rem; } - .pa5-ns { padding: 4rem; } - .pa6-ns { padding: 8rem; } - .pa7-ns { padding: 16rem; } - - .pl0-ns { padding-left: 0; } - .pl1-ns { padding-left: .25rem; } - .pl2-ns { padding-left: .5rem; } - .pl3-ns { padding-left: 1rem; } - .pl4-ns { padding-left: 2rem; } - .pl5-ns { padding-left: 4rem; } - .pl6-ns { padding-left: 8rem; } - .pl7-ns { padding-left: 16rem; } - - .pr0-ns { padding-right: 0; } - .pr1-ns { padding-right: .25rem; } - .pr2-ns { padding-right: .5rem; } - .pr3-ns { padding-right: 1rem; } - .pr4-ns { padding-right: 2rem; } - .pr5-ns { padding-right: 4rem; } - .pr6-ns { padding-right: 8rem; } - .pr7-ns { padding-right: 16rem; } - - .pb0-ns { padding-bottom: 0; } - .pb1-ns { padding-bottom: .25rem; } - .pb2-ns { padding-bottom: .5rem; } - .pb3-ns { padding-bottom: 1rem; } - .pb4-ns { padding-bottom: 2rem; } - .pb5-ns { padding-bottom: 4rem; } - .pb6-ns { padding-bottom: 8rem; } - .pb7-ns { padding-bottom: 16rem; } - - .pt0-ns { padding-top: 0; } - .pt1-ns { padding-top: .25rem; } - .pt2-ns { padding-top: .5rem; } - .pt3-ns { padding-top: 1rem; } - .pt4-ns { padding-top: 2rem; } - .pt5-ns { padding-top: 4rem; } - .pt6-ns { padding-top: 8rem; } - .pt7-ns { padding-top: 16rem; } - - .pv0-ns { - padding-top: 0; - padding-bottom: 0; - } - .pv1-ns { - padding-top: .25rem; - padding-bottom: .25rem; - } - .pv2-ns { - padding-top: .5rem; - padding-bottom: .5rem; - } - .pv3-ns { - padding-top: 1rem; - padding-bottom: 1rem; - } - .pv4-ns { - padding-top: 2rem; - padding-bottom: 2rem; - } - .pv5-ns { - padding-top: 4rem; - padding-bottom: 4rem; - } - .pv6-ns { - padding-top: 8rem; - padding-bottom: 8rem; - } - .pv7-ns { - padding-top: 16rem; - padding-bottom: 16rem; - } - .ph0-ns { - padding-left: 0; - padding-right: 0; - } - .ph1-ns { - padding-left: .25rem; - padding-right: .25rem; - } - .ph2-ns { - padding-left: .5rem; - padding-right: .5rem; - } - .ph3-ns { - padding-left: 1rem; - padding-right: 1rem; - } - .ph4-ns { - padding-left: 2rem; - padding-right: 2rem; - } - .ph5-ns { - padding-left: 4rem; - padding-right: 4rem; - } - .ph6-ns { - padding-left: 8rem; - padding-right: 8rem; - } - .ph7-ns { - padding-left: 16rem; - padding-right: 16rem; - } - - .ma0-ns { margin: 0; } - .ma1-ns { margin: .25rem; } - .ma2-ns { margin: .5rem; } - .ma3-ns { margin: 1rem; } - .ma4-ns { margin: 2rem; } - .ma5-ns { margin: 4rem; } - .ma6-ns { margin: 8rem; } - .ma7-ns { margin: 16rem; } - - .ml0-ns { margin-left: 0; } - .ml1-ns { margin-left: .25rem; } - .ml2-ns { margin-left: .5rem; } - .ml3-ns { margin-left: 1rem; } - .ml4-ns { margin-left: 2rem; } - .ml5-ns { margin-left: 4rem; } - .ml6-ns { margin-left: 8rem; } - .ml7-ns { margin-left: 16rem; } - - .mr0-ns { margin-right: 0; } - .mr1-ns { margin-right: .25rem; } - .mr2-ns { margin-right: .5rem; } - .mr3-ns { margin-right: 1rem; } - .mr4-ns { margin-right: 2rem; } - .mr5-ns { margin-right: 4rem; } - .mr6-ns { margin-right: 8rem; } - .mr7-ns { margin-right: 16rem; } - - .mb0-ns { margin-bottom: 0; } - .mb1-ns { margin-bottom: .25rem; } - .mb2-ns { margin-bottom: .5rem; } - .mb3-ns { margin-bottom: 1rem; } - .mb4-ns { margin-bottom: 2rem; } - .mb5-ns { margin-bottom: 4rem; } - .mb6-ns { margin-bottom: 8rem; } - .mb7-ns { margin-bottom: 16rem; } - - .mt0-ns { margin-top: 0; } - .mt1-ns { margin-top: .25rem; } - .mt2-ns { margin-top: .5rem; } - .mt3-ns { margin-top: 1rem; } - .mt4-ns { margin-top: 2rem; } - .mt5-ns { margin-top: 4rem; } - .mt6-ns { margin-top: 8rem; } - .mt7-ns { margin-top: 16rem; } - - .mv0-ns { - margin-top: 0; - margin-bottom: 0; - } - .mv1-ns { - margin-top: .25rem; - margin-bottom: .25rem; - } - .mv2-ns { - margin-top: .5rem; - margin-bottom: .5rem; - } - .mv3-ns { - margin-top: 1rem; - margin-bottom: 1rem; - } - .mv4-ns { - margin-top: 2rem; - margin-bottom: 2rem; - } - .mv5-ns { - margin-top: 4rem; - margin-bottom: 4rem; - } - .mv6-ns { - margin-top: 8rem; - margin-bottom: 8rem; - } - .mv7-ns { - margin-top: 16rem; - margin-bottom: 16rem; - } - - .mh0-ns { - margin-left: 0; - margin-right: 0; - } - .mh1-ns { - margin-left: .25rem; - margin-right: .25rem; - } - .mh2-ns { - margin-left: .5rem; - margin-right: .5rem; - } - .mh3-ns { - margin-left: 1rem; - margin-right: 1rem; - } - .mh4-ns { - margin-left: 2rem; - margin-right: 2rem; - } - .mh5-ns { - margin-left: 4rem; - margin-right: 4rem; - } - .mh6-ns { - margin-left: 8rem; - margin-right: 8rem; - } - .mh7-ns { - margin-left: 16rem; - margin-right: 16rem; - } - -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .pa0-m { padding: 0; } - .pa1-m { padding: .25rem; } - .pa2-m { padding: .5rem; } - .pa3-m { padding: 1rem; } - .pa4-m { padding: 2rem; } - .pa5-m { padding: 4rem; } - .pa6-m { padding: 8rem; } - .pa7-m { padding: 16rem; } - - .pl0-m { padding-left: 0; } - .pl1-m { padding-left: .25rem; } - .pl2-m { padding-left: .5rem; } - .pl3-m { padding-left: 1rem; } - .pl4-m { padding-left: 2rem; } - .pl5-m { padding-left: 4rem; } - .pl6-m { padding-left: 8rem; } - .pl7-m { padding-left: 16rem; } - - .pr0-m { padding-right: 0; } - .pr1-m { padding-right: .25rem; } - .pr2-m { padding-right: .5rem; } - .pr3-m { padding-right: 1rem; } - .pr4-m { padding-right: 2rem; } - .pr5-m { padding-right: 4rem; } - .pr6-m { padding-right: 8rem; } - .pr7-m { padding-right: 16rem; } - - .pb0-m { padding-bottom: 0; } - .pb1-m { padding-bottom: .25rem; } - .pb2-m { padding-bottom: .5rem; } - .pb3-m { padding-bottom: 1rem; } - .pb4-m { padding-bottom: 2rem; } - .pb5-m { padding-bottom: 4rem; } - .pb6-m { padding-bottom: 8rem; } - .pb7-m { padding-bottom: 16rem; } - - .pt0-m { padding-top: 0; } - .pt1-m { padding-top: .25rem; } - .pt2-m { padding-top: .5rem; } - .pt3-m { padding-top: 1rem; } - .pt4-m { padding-top: 2rem; } - .pt5-m { padding-top: 4rem; } - .pt6-m { padding-top: 8rem; } - .pt7-m { padding-top: 16rem; } - - .pv0-m { - padding-top: 0; - padding-bottom: 0; - } - .pv1-m { - padding-top: .25rem; - padding-bottom: .25rem; - } - .pv2-m { - padding-top: .5rem; - padding-bottom: .5rem; - } - .pv3-m { - padding-top: 1rem; - padding-bottom: 1rem; - } - .pv4-m { - padding-top: 2rem; - padding-bottom: 2rem; - } - .pv5-m { - padding-top: 4rem; - padding-bottom: 4rem; - } - .pv6-m { - padding-top: 8rem; - padding-bottom: 8rem; - } - .pv7-m { - padding-top: 16rem; - padding-bottom: 16rem; - } - - .ph0-m { - padding-left: 0; - padding-right: 0; - } - .ph1-m { - padding-left: .25rem; - padding-right: .25rem; - } - .ph2-m { - padding-left: .5rem; - padding-right: .5rem; - } - .ph3-m { - padding-left: 1rem; - padding-right: 1rem; - } - .ph4-m { - padding-left: 2rem; - padding-right: 2rem; - } - .ph5-m { - padding-left: 4rem; - padding-right: 4rem; - } - .ph6-m { - padding-left: 8rem; - padding-right: 8rem; - } - .ph7-m { - padding-left: 16rem; - padding-right: 16rem; - } - - .ma0-m { margin: 0; } - .ma1-m { margin: .25rem; } - .ma2-m { margin: .5rem; } - .ma3-m { margin: 1rem; } - .ma4-m { margin: 2rem; } - .ma5-m { margin: 4rem; } - .ma6-m { margin: 8rem; } - .ma7-m { margin: 16rem; } - - .ml0-m { margin-left: 0; } - .ml1-m { margin-left: .25rem; } - .ml2-m { margin-left: .5rem; } - .ml3-m { margin-left: 1rem; } - .ml4-m { margin-left: 2rem; } - .ml5-m { margin-left: 4rem; } - .ml6-m { margin-left: 8rem; } - .ml7-m { margin-left: 16rem; } - - .mr0-m { margin-right: 0; } - .mr1-m { margin-right: .25rem; } - .mr2-m { margin-right: .5rem; } - .mr3-m { margin-right: 1rem; } - .mr4-m { margin-right: 2rem; } - .mr5-m { margin-right: 4rem; } - .mr6-m { margin-right: 8rem; } - .mr7-m { margin-right: 16rem; } - - .mb0-m { margin-bottom: 0; } - .mb1-m { margin-bottom: .25rem; } - .mb2-m { margin-bottom: .5rem; } - .mb3-m { margin-bottom: 1rem; } - .mb4-m { margin-bottom: 2rem; } - .mb5-m { margin-bottom: 4rem; } - .mb6-m { margin-bottom: 8rem; } - .mb7-m { margin-bottom: 16rem; } - - .mt0-m { margin-top: 0; } - .mt1-m { margin-top: .25rem; } - .mt2-m { margin-top: .5rem; } - .mt3-m { margin-top: 1rem; } - .mt4-m { margin-top: 2rem; } - .mt5-m { margin-top: 4rem; } - .mt6-m { margin-top: 8rem; } - .mt7-m { margin-top: 16rem; } - - .mv0-m { - margin-top: 0; - margin-bottom: 0; - } - .mv1-m { - margin-top: .25rem; - margin-bottom: .25rem; - } - .mv2-m { - margin-top: .5rem; - margin-bottom: .5rem; - } - .mv3-m { - margin-top: 1rem; - margin-bottom: 1rem; - } - .mv4-m { - margin-top: 2rem; - margin-bottom: 2rem; - } - .mv5-m { - margin-top: 4rem; - margin-bottom: 4rem; - } - .mv6-m { - margin-top: 8rem; - margin-bottom: 8rem; - } - .mv7-m { - margin-top: 16rem; - margin-bottom: 16rem; - } - - .mh0-m { - margin-left: 0; - margin-right: 0; - } - .mh1-m { - margin-left: .25rem; - margin-right: .25rem; - } - .mh2-m { - margin-left: .5rem; - margin-right: .5rem; - } - .mh3-m { - margin-left: 1rem; - margin-right: 1rem; - } - .mh4-m { - margin-left: 2rem; - margin-right: 2rem; - } - .mh5-m { - margin-left: 4rem; - margin-right: 4rem; - } - .mh6-m { - margin-left: 8rem; - margin-right: 8rem; - } - .mh7-m { - margin-left: 16rem; - margin-right: 16rem; - } - -} -@media screen and (min-width: 60em) { - .pa0-l { padding: 0; } - .pa1-l { padding: .25rem; } - .pa2-l { padding: .5rem; } - .pa3-l { padding: 1rem; } - .pa4-l { padding: 2rem; } - .pa5-l { padding: 4rem; } - .pa6-l { padding: 8rem; } - .pa7-l { padding: 16rem; } - - .pl0-l { padding-left: 0; } - .pl1-l { padding-left: .25rem; } - .pl2-l { padding-left: .5rem; } - .pl3-l { padding-left: 1rem; } - .pl4-l { padding-left: 2rem; } - .pl5-l { padding-left: 4rem; } - .pl6-l { padding-left: 8rem; } - .pl7-l { padding-left: 16rem; } - - .pr0-l { padding-right: 0; } - .pr1-l { padding-right: .25rem; } - .pr2-l { padding-right: .5rem; } - .pr3-l { padding-right: 1rem; } - .pr4-l { padding-right: 2rem; } - .pr5-l { padding-right: 4rem; } - .pr6-l { padding-right: 8rem; } - .pr7-l { padding-right: 16rem; } - - .pb0-l { padding-bottom: 0; } - .pb1-l { padding-bottom: .25rem; } - .pb2-l { padding-bottom: .5rem; } - .pb3-l { padding-bottom: 1rem; } - .pb4-l { padding-bottom: 2rem; } - .pb5-l { padding-bottom: 4rem; } - .pb6-l { padding-bottom: 8rem; } - .pb7-l { padding-bottom: 16rem; } - - .pt0-l { padding-top: 0; } - .pt1-l { padding-top: .25rem; } - .pt2-l { padding-top: .5rem; } - .pt3-l { padding-top: 1rem; } - .pt4-l { padding-top: 2rem; } - .pt5-l { padding-top: 4rem; } - .pt6-l { padding-top: 8rem; } - .pt7-l { padding-top: 16rem; } - - .pv0-l { - padding-top: 0; - padding-bottom: 0; - } - .pv1-l { - padding-top: .25rem; - padding-bottom: .25rem; - } - .pv2-l { - padding-top: .5rem; - padding-bottom: .5rem; - } - .pv3-l { - padding-top: 1rem; - padding-bottom: 1rem; - } - .pv4-l { - padding-top: 2rem; - padding-bottom: 2rem; - } - .pv5-l { - padding-top: 4rem; - padding-bottom: 4rem; - } - .pv6-l { - padding-top: 8rem; - padding-bottom: 8rem; - } - .pv7-l { - padding-top: 16rem; - padding-bottom: 16rem; - } - - .ph0-l { - padding-left: 0; - padding-right: 0; - } - .ph1-l { - padding-left: .25rem; - padding-right: .25rem; - } - .ph2-l { - padding-left: .5rem; - padding-right: .5rem; - } - .ph3-l { - padding-left: 1rem; - padding-right: 1rem; - } - .ph4-l { - padding-left: 2rem; - padding-right: 2rem; - } - .ph5-l { - padding-left: 4rem; - padding-right: 4rem; - } - .ph6-l { - padding-left: 8rem; - padding-right: 8rem; - } - .ph7-l { - padding-left: 16rem; - padding-right: 16rem; - } - - .ma0-l { margin: 0; } - .ma1-l { margin: .25rem; } - .ma2-l { margin: .5rem; } - .ma3-l { margin: 1rem; } - .ma4-l { margin: 2rem; } - .ma5-l { margin: 4rem; } - .ma6-l { margin: 8rem; } - .ma7-l { margin: 16rem; } - - .ml0-l { margin-left: 0; } - .ml1-l { margin-left: .25rem; } - .ml2-l { margin-left: .5rem; } - .ml3-l { margin-left: 1rem; } - .ml4-l { margin-left: 2rem; } - .ml5-l { margin-left: 4rem; } - .ml6-l { margin-left: 8rem; } - .ml7-l { margin-left: 16rem; } - - .mr0-l { margin-right: 0; } - .mr1-l { margin-right: .25rem; } - .mr2-l { margin-right: .5rem; } - .mr3-l { margin-right: 1rem; } - .mr4-l { margin-right: 2rem; } - .mr5-l { margin-right: 4rem; } - .mr6-l { margin-right: 8rem; } - .mr7-l { margin-right: 16rem; } - - .mb0-l { margin-bottom: 0; } - .mb1-l { margin-bottom: .25rem; } - .mb2-l { margin-bottom: .5rem; } - .mb3-l { margin-bottom: 1rem; } - .mb4-l { margin-bottom: 2rem; } - .mb5-l { margin-bottom: 4rem; } - .mb6-l { margin-bottom: 8rem; } - .mb7-l { margin-bottom: 16rem; } - - .mt0-l { margin-top: 0; } - .mt1-l { margin-top: .25rem; } - .mt2-l { margin-top: .5rem; } - .mt3-l { margin-top: 1rem; } - .mt4-l { margin-top: 2rem; } - .mt5-l { margin-top: 4rem; } - .mt6-l { margin-top: 8rem; } - .mt7-l { margin-top: 16rem; } - - .mv0-l { - margin-top: 0; - margin-bottom: 0; - } - .mv1-l { - margin-top: .25rem; - margin-bottom: .25rem; - } - .mv2-l { - margin-top: .5rem; - margin-bottom: .5rem; - } - .mv3-l { - margin-top: 1rem; - margin-bottom: 1rem; - } - .mv4-l { - margin-top: 2rem; - margin-bottom: 2rem; - } - .mv5-l { - margin-top: 4rem; - margin-bottom: 4rem; - } - .mv6-l { - margin-top: 8rem; - margin-bottom: 8rem; - } - .mv7-l { - margin-top: 16rem; - margin-bottom: 16rem; - } - - .mh0-l { - margin-left: 0; - margin-right: 0; - } - .mh1-l { - margin-left: .25rem; - margin-right: .25rem; - } - .mh2-l { - margin-left: .5rem; - margin-right: .5rem; - } - .mh3-l { - margin-left: 1rem; - margin-right: 1rem; - } - .mh4-l { - margin-left: 2rem; - margin-right: 2rem; - } - .mh5-l { - margin-left: 4rem; - margin-right: 4rem; - } - .mh6-l { - margin-left: 8rem; - margin-right: 8rem; - } - .mh7-l { - margin-left: 16rem; - margin-right: 16rem; - } -} -/* - NEGATIVE MARGINS - - Base: - n = negative - - Modifiers: - a = all - t = top - r = right - b = bottom - l = left - - 1 = 1st step in spacing scale - 2 = 2nd step in spacing scale - 3 = 3rd step in spacing scale - 4 = 4th step in spacing scale - 5 = 5th step in spacing scale - 6 = 6th step in spacing scale - 7 = 7th step in spacing scale - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.na1 { margin: -0.25rem; } -.na2 { margin: -0.5rem; } -.na3 { margin: -1rem; } -.na4 { margin: -2rem; } -.na5 { margin: -4rem; } -.na6 { margin: -8rem; } -.na7 { margin: -16rem; } -.nl1 { margin-left: -0.25rem; } -.nl2 { margin-left: -0.5rem; } -.nl3 { margin-left: -1rem; } -.nl4 { margin-left: -2rem; } -.nl5 { margin-left: -4rem; } -.nl6 { margin-left: -8rem; } -.nl7 { margin-left: -16rem; } -.nr1 { margin-right: -0.25rem; } -.nr2 { margin-right: -0.5rem; } -.nr3 { margin-right: -1rem; } -.nr4 { margin-right: -2rem; } -.nr5 { margin-right: -4rem; } -.nr6 { margin-right: -8rem; } -.nr7 { margin-right: -16rem; } -.nb1 { margin-bottom: -0.25rem; } -.nb2 { margin-bottom: -0.5rem; } -.nb3 { margin-bottom: -1rem; } -.nb4 { margin-bottom: -2rem; } -.nb5 { margin-bottom: -4rem; } -.nb6 { margin-bottom: -8rem; } -.nb7 { margin-bottom: -16rem; } -.nt1 { margin-top: -0.25rem; } -.nt2 { margin-top: -0.5rem; } -.nt3 { margin-top: -1rem; } -.nt4 { margin-top: -2rem; } -.nt5 { margin-top: -4rem; } -.nt6 { margin-top: -8rem; } -.nt7 { margin-top: -16rem; } -@media screen and (min-width: 30em) { - - .na1-ns { margin: -0.25rem; } - .na2-ns { margin: -0.5rem; } - .na3-ns { margin: -1rem; } - .na4-ns { margin: -2rem; } - .na5-ns { margin: -4rem; } - .na6-ns { margin: -8rem; } - .na7-ns { margin: -16rem; } - - .nl1-ns { margin-left: -0.25rem; } - .nl2-ns { margin-left: -0.5rem; } - .nl3-ns { margin-left: -1rem; } - .nl4-ns { margin-left: -2rem; } - .nl5-ns { margin-left: -4rem; } - .nl6-ns { margin-left: -8rem; } - .nl7-ns { margin-left: -16rem; } - - .nr1-ns { margin-right: -0.25rem; } - .nr2-ns { margin-right: -0.5rem; } - .nr3-ns { margin-right: -1rem; } - .nr4-ns { margin-right: -2rem; } - .nr5-ns { margin-right: -4rem; } - .nr6-ns { margin-right: -8rem; } - .nr7-ns { margin-right: -16rem; } - - .nb1-ns { margin-bottom: -0.25rem; } - .nb2-ns { margin-bottom: -0.5rem; } - .nb3-ns { margin-bottom: -1rem; } - .nb4-ns { margin-bottom: -2rem; } - .nb5-ns { margin-bottom: -4rem; } - .nb6-ns { margin-bottom: -8rem; } - .nb7-ns { margin-bottom: -16rem; } - - .nt1-ns { margin-top: -0.25rem; } - .nt2-ns { margin-top: -0.5rem; } - .nt3-ns { margin-top: -1rem; } - .nt4-ns { margin-top: -2rem; } - .nt5-ns { margin-top: -4rem; } - .nt6-ns { margin-top: -8rem; } - .nt7-ns { margin-top: -16rem; } - -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .na1-m { margin: -0.25rem; } - .na2-m { margin: -0.5rem; } - .na3-m { margin: -1rem; } - .na4-m { margin: -2rem; } - .na5-m { margin: -4rem; } - .na6-m { margin: -8rem; } - .na7-m { margin: -16rem; } - - .nl1-m { margin-left: -0.25rem; } - .nl2-m { margin-left: -0.5rem; } - .nl3-m { margin-left: -1rem; } - .nl4-m { margin-left: -2rem; } - .nl5-m { margin-left: -4rem; } - .nl6-m { margin-left: -8rem; } - .nl7-m { margin-left: -16rem; } - - .nr1-m { margin-right: -0.25rem; } - .nr2-m { margin-right: -0.5rem; } - .nr3-m { margin-right: -1rem; } - .nr4-m { margin-right: -2rem; } - .nr5-m { margin-right: -4rem; } - .nr6-m { margin-right: -8rem; } - .nr7-m { margin-right: -16rem; } - - .nb1-m { margin-bottom: -0.25rem; } - .nb2-m { margin-bottom: -0.5rem; } - .nb3-m { margin-bottom: -1rem; } - .nb4-m { margin-bottom: -2rem; } - .nb5-m { margin-bottom: -4rem; } - .nb6-m { margin-bottom: -8rem; } - .nb7-m { margin-bottom: -16rem; } - - .nt1-m { margin-top: -0.25rem; } - .nt2-m { margin-top: -0.5rem; } - .nt3-m { margin-top: -1rem; } - .nt4-m { margin-top: -2rem; } - .nt5-m { margin-top: -4rem; } - .nt6-m { margin-top: -8rem; } - .nt7-m { margin-top: -16rem; } - -} -@media screen and (min-width: 60em) { - .na1-l { margin: -0.25rem; } - .na2-l { margin: -0.5rem; } - .na3-l { margin: -1rem; } - .na4-l { margin: -2rem; } - .na5-l { margin: -4rem; } - .na6-l { margin: -8rem; } - .na7-l { margin: -16rem; } - - .nl1-l { margin-left: -0.25rem; } - .nl2-l { margin-left: -0.5rem; } - .nl3-l { margin-left: -1rem; } - .nl4-l { margin-left: -2rem; } - .nl5-l { margin-left: -4rem; } - .nl6-l { margin-left: -8rem; } - .nl7-l { margin-left: -16rem; } - - .nr1-l { margin-right: -0.25rem; } - .nr2-l { margin-right: -0.5rem; } - .nr3-l { margin-right: -1rem; } - .nr4-l { margin-right: -2rem; } - .nr5-l { margin-right: -4rem; } - .nr6-l { margin-right: -8rem; } - .nr7-l { margin-right: -16rem; } - - .nb1-l { margin-bottom: -0.25rem; } - .nb2-l { margin-bottom: -0.5rem; } - .nb3-l { margin-bottom: -1rem; } - .nb4-l { margin-bottom: -2rem; } - .nb5-l { margin-bottom: -4rem; } - .nb6-l { margin-bottom: -8rem; } - .nb7-l { margin-bottom: -16rem; } - - .nt1-l { margin-top: -0.25rem; } - .nt2-l { margin-top: -0.5rem; } - .nt3-l { margin-top: -1rem; } - .nt4-l { margin-top: -2rem; } - .nt5-l { margin-top: -4rem; } - .nt6-l { margin-top: -8rem; } - .nt7-l { margin-top: -16rem; } -} -/* - - TABLES - Docs: http://tachyons.io/docs/elements/tables/ - -*/ -.collapse { - border-collapse: collapse; - border-spacing: 0; -} -.striped--light-silver:nth-child(odd) { - background-color: #aaa; -} -.striped--moon-gray:nth-child(odd) { - background-color: #ccc; -} -.striped--light-gray:nth-child(odd) { - background-color: #eee; -} -.striped--near-white:nth-child(odd) { - background-color: #f4f4f4; -} -.stripe-light:nth-child(odd) { - background-color: rgba(255, 255, 255, .1); -} -.stripe-dark:nth-child(odd) { - background-color: rgba(0, 0, 0, .1); -} -/* - - TEXT DECORATION - Docs: http://tachyons.io/docs/typography/text-decoration/ - - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.strike { text-decoration: line-through; } -.underline { text-decoration: underline; } -.no-underline { text-decoration: none; } -@media screen and (min-width: 30em) { - .strike-ns { text-decoration: line-through; } - .underline-ns { text-decoration: underline; } - .no-underline-ns { text-decoration: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .strike-m { text-decoration: line-through; } - .underline-m { text-decoration: underline; } - .no-underline-m { text-decoration: none; } -} -@media screen and (min-width: 60em) { - .strike-l { text-decoration: line-through; } - .underline-l { text-decoration: underline; } - .no-underline-l { text-decoration: none; } -} -/* - - TEXT ALIGN - Docs: http://tachyons.io/docs/typography/text-align/ - - Base - t = text-align - - Modifiers - l = left - r = right - c = center - j = justify - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.tl { text-align: left; } -.tr { text-align: right; } -.tc { text-align: center; } -.tj { text-align: justify; } -@media screen and (min-width: 30em) { - .tl-ns { text-align: left; } - .tr-ns { text-align: right; } - .tc-ns { text-align: center; } - .tj-ns { text-align: justify; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .tl-m { text-align: left; } - .tr-m { text-align: right; } - .tc-m { text-align: center; } - .tj-m { text-align: justify; } -} -@media screen and (min-width: 60em) { - .tl-l { text-align: left; } - .tr-l { text-align: right; } - .tc-l { text-align: center; } - .tj-l { text-align: justify; } -} -/* - - TEXT TRANSFORM - Docs: http://tachyons.io/docs/typography/text-transform/ - - Base: - tt = text-transform - - Modifiers - c = capitalize - l = lowercase - u = uppercase - n = none - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.ttc { text-transform: capitalize; } -.ttl { text-transform: lowercase; } -.ttu { text-transform: uppercase; } -.ttn { text-transform: none; } -@media screen and (min-width: 30em) { - .ttc-ns { text-transform: capitalize; } - .ttl-ns { text-transform: lowercase; } - .ttu-ns { text-transform: uppercase; } - .ttn-ns { text-transform: none; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .ttc-m { text-transform: capitalize; } - .ttl-m { text-transform: lowercase; } - .ttu-m { text-transform: uppercase; } - .ttn-m { text-transform: none; } -} -@media screen and (min-width: 60em) { - .ttc-l { text-transform: capitalize; } - .ttl-l { text-transform: lowercase; } - .ttu-l { text-transform: uppercase; } - .ttn-l { text-transform: none; } -} -/* - - TYPE SCALE - Docs: http://tachyons.io/docs/typography/scale/ - - Base: - f = font-size - - Modifiers - 1 = 1st step in size scale - 2 = 2nd step in size scale - 3 = 3rd step in size scale - 4 = 4th step in size scale - 5 = 5th step in size scale - 6 = 6th step in size scale - 7 = 7th step in size scale - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large -*/ -/* - * For Hero/Marketing Titles - * - * These generally are too large for mobile - * so be careful using them on smaller screens. - * */ -.f-6, -.f-headline { - font-size: 6rem; -} -.f-5, -.f-subheadline { - font-size: 5rem; -} -/* Type Scale */ -.f1 { font-size: 3rem; } -.f2 { font-size: 2.25rem; } -.f3 { font-size: 1.5rem; } -.f4 { font-size: 1.25rem; } -.f5 { font-size: 1rem; } -.f6 { font-size: .875rem; } -.f7 { font-size: .75rem; } -/* Small and hard to read for many people so use with extreme caution */ -@media screen and (min-width: 30em){ - .f-6-ns, - .f-headline-ns { font-size: 6rem; } - .f-5-ns, - .f-subheadline-ns { font-size: 5rem; } - .f1-ns { font-size: 3rem; } - .f2-ns { font-size: 2.25rem; } - .f3-ns { font-size: 1.5rem; } - .f4-ns { font-size: 1.25rem; } - .f5-ns { font-size: 1rem; } - .f6-ns { font-size: .875rem; } - .f7-ns { font-size: .75rem; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .f-6-m, - .f-headline-m { font-size: 6rem; } - .f-5-m, - .f-subheadline-m { font-size: 5rem; } - .f1-m { font-size: 3rem; } - .f2-m { font-size: 2.25rem; } - .f3-m { font-size: 1.5rem; } - .f4-m { font-size: 1.25rem; } - .f5-m { font-size: 1rem; } - .f6-m { font-size: .875rem; } - .f7-m { font-size: .75rem; } -} -@media screen and (min-width: 60em) { - .f-6-l, - .f-headline-l { - font-size: 6rem; - } - .f-5-l, - .f-subheadline-l { - font-size: 5rem; - } - .f1-l { font-size: 3rem; } - .f2-l { font-size: 2.25rem; } - .f3-l { font-size: 1.5rem; } - .f4-l { font-size: 1.25rem; } - .f5-l { font-size: 1rem; } - .f6-l { font-size: .875rem; } - .f7-l { font-size: .75rem; } -} -/* - - TYPOGRAPHY - http://tachyons.io/docs/typography/measure/ - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* Measure is limited to ~66 characters */ -.measure { - max-width: 30em; -} -/* Measure is limited to ~80 characters */ -.measure-wide { - max-width: 34em; -} -/* Measure is limited to ~45 characters */ -.measure-narrow { - max-width: 20em; -} -/* Book paragraph style - paragraphs are indented with no vertical spacing. */ -.indent { - text-indent: 1em; - margin-top: 0; - margin-bottom: 0; -} -.small-caps { - -webkit-font-feature-settings: "c2sc"; - font-feature-settings: "c2sc"; - font-variant: small-caps; -} -/* Combine this class with a width to truncate text (or just leave as is to truncate at width of containing element. */ -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -@media screen and (min-width: 30em) { - .measure-ns { - max-width: 30em; - } - .measure-wide-ns { - max-width: 34em; - } - .measure-narrow-ns { - max-width: 20em; - } - .indent-ns { - text-indent: 1em; - margin-top: 0; - margin-bottom: 0; - } - .small-caps-ns { - -webkit-font-feature-settings: "c2sc"; - font-feature-settings: "c2sc"; - font-variant: small-caps; - } - .truncate-ns { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .measure-m { - max-width: 30em; - } - .measure-wide-m { - max-width: 34em; - } - .measure-narrow-m { - max-width: 20em; - } - .indent-m { - text-indent: 1em; - margin-top: 0; - margin-bottom: 0; - } - .small-caps-m { - -webkit-font-feature-settings: "c2sc"; - font-feature-settings: "c2sc"; - font-variant: small-caps; - } - .truncate-m { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} -@media screen and (min-width: 60em) { - .measure-l { - max-width: 30em; - } - .measure-wide-l { - max-width: 34em; - } - .measure-narrow-l { - max-width: 20em; - } - .indent-l { - text-indent: 1em; - margin-top: 0; - margin-bottom: 0; - } - .small-caps-l { - -webkit-font-feature-settings: "c2sc"; - font-feature-settings: "c2sc"; - font-variant: small-caps; - } - .truncate-l { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} -/* - - UTILITIES - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* Equivalent to .overflow-y-scroll */ -.overflow-container { - overflow-y: scroll; -} -.center { - margin-right: auto; - margin-left: auto; -} -.mr-auto { margin-right: auto; } -.ml-auto { margin-left: auto; } -@media screen and (min-width: 30em){ - .center-ns { - margin-right: auto; - margin-left: auto; - } - .mr-auto-ns { margin-right: auto; } - .ml-auto-ns { margin-left: auto; } -} -@media screen and (min-width: 30em) and (max-width: 60em){ - .center-m { - margin-right: auto; - margin-left: auto; - } - .mr-auto-m { margin-right: auto; } - .ml-auto-m { margin-left: auto; } -} -@media screen and (min-width: 60em){ - .center-l { - margin-right: auto; - margin-left: auto; - } - .mr-auto-l { margin-right: auto; } - .ml-auto-l { margin-left: auto; } -} -/* - - VISIBILITY - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -/* - Text that is hidden but accessible - Ref: http://snook.ca/archives/html_and_css/hiding-content-for-accessibility -*/ -.clip { - position: fixed !important; - _position: absolute !important; - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); -} -@media screen and (min-width: 30em) { - .clip-ns { - position: fixed !important; - _position: absolute !important; - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); - } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .clip-m { - position: fixed !important; - _position: absolute !important; - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); - } -} -@media screen and (min-width: 60em) { - .clip-l { - position: fixed !important; - _position: absolute !important; - clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ - clip: rect(1px, 1px, 1px, 1px); - } -} -/* - - WHITE SPACE - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.ws-normal { white-space: normal; } -.nowrap { white-space: nowrap; } -.pre { white-space: pre; } -@media screen and (min-width: 30em) { - .ws-normal-ns { white-space: normal; } - .nowrap-ns { white-space: nowrap; } - .pre-ns { white-space: pre; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .ws-normal-m { white-space: normal; } - .nowrap-m { white-space: nowrap; } - .pre-m { white-space: pre; } -} -@media screen and (min-width: 60em) { - .ws-normal-l { white-space: normal; } - .nowrap-l { white-space: nowrap; } - .pre-l { white-space: pre; } -} -/* - - VERTICAL ALIGN - - Media Query Extensions: - -ns = not-small - -m = medium - -l = large - -*/ -.v-base { vertical-align: baseline; } -.v-mid { vertical-align: middle; } -.v-top { vertical-align: top; } -.v-btm { vertical-align: bottom; } -@media screen and (min-width: 30em) { - .v-base-ns { vertical-align: baseline; } - .v-mid-ns { vertical-align: middle; } - .v-top-ns { vertical-align: top; } - .v-btm-ns { vertical-align: bottom; } -} -@media screen and (min-width: 30em) and (max-width: 60em) { - .v-base-m { vertical-align: baseline; } - .v-mid-m { vertical-align: middle; } - .v-top-m { vertical-align: top; } - .v-btm-m { vertical-align: bottom; } -} -@media screen and (min-width: 60em) { - .v-base-l { vertical-align: baseline; } - .v-mid-l { vertical-align: middle; } - .v-top-l { vertical-align: top; } - .v-btm-l { vertical-align: bottom; } -} -/* - - HOVER EFFECTS - Docs: http://tachyons.io/docs/themes/hovers/ - - - Dim - - Glow - - Hide Child - - Underline text - - Grow - - Pointer - - Shadow - -*/ -/* - - Dim element on hover by adding the dim class. - -*/ -.dim { - opacity: 1; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.dim:hover, -.dim:focus { - opacity: .5; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.dim:active { - opacity: .8; -webkit-transition: opacity .15s ease-out; transition: opacity .15s ease-out; -} -/* - - Animate opacity to 100% on hover by adding the glow class. - -*/ -.glow { - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.glow:hover, -.glow:focus { - opacity: 1; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -/* - - Hide child & reveal on hover: - - Put the hide-child class on a parent element and any nested element with the - child class will be hidden and displayed on hover or focus. - -
    -
    Hidden until hover or focus
    -
    Hidden until hover or focus
    -
    Hidden until hover or focus
    -
    Hidden until hover or focus
    -
    -*/ -.hide-child .child { - opacity: 0; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.hide-child:hover .child, -.hide-child:focus .child, -.hide-child:active .child { - opacity: 1; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.underline-hover:hover, -.underline-hover:focus { - text-decoration: underline; -} -/* Can combine this with overflow-hidden to make background images grow on hover - * even if you are using background-size: cover */ -.grow { - -moz-osx-font-smoothing: grayscale; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transform: translateZ(0); - transform: translateZ(0); - -webkit-transition: -webkit-transform 0.25s ease-out; - transition: -webkit-transform 0.25s ease-out; - transition: transform 0.25s ease-out; - transition: transform 0.25s ease-out, -webkit-transform 0.25s ease-out; -} -.grow:hover, -.grow:focus { - -webkit-transform: scale(1.05); - transform: scale(1.05); -} -.grow:active { - -webkit-transform: scale(.90); - transform: scale(.90); -} -.grow-large { - -moz-osx-font-smoothing: grayscale; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transform: translateZ(0); - transform: translateZ(0); - -webkit-transition: -webkit-transform .25s ease-in-out; - transition: -webkit-transform .25s ease-in-out; - transition: transform .25s ease-in-out; - transition: transform .25s ease-in-out, -webkit-transform .25s ease-in-out; -} -.grow-large:hover, -.grow-large:focus { - -webkit-transform: scale(1.2); - transform: scale(1.2); -} -.grow-large:active { - -webkit-transform: scale(.95); - transform: scale(.95); -} -/* Add pointer on hover */ -.pointer:hover { - cursor: pointer; -} -/* - Add shadow on hover. - - Performant box-shadow animation pattern from - http://tobiasahlin.com/blog/how-to-animate-box-shadow/ -*/ -.shadow-hover { - cursor: pointer; - position: relative; - -webkit-transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); - transition: all 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); -} -.shadow-hover::after { - content: ''; - -webkit-box-shadow: 0px 0px 16px 2px rgba(0, 0, 0, .2); - box-shadow: 0px 0px 16px 2px rgba(0, 0, 0, .2); - border-radius: inherit; - opacity: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -1; - -webkit-transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); - transition: opacity 0.5s cubic-bezier(0.165, 0.84, 0.44, 1); -} -.shadow-hover:hover::after, -.shadow-hover:focus::after { - opacity: 1; -} -/* Combine with classes in skins and skins-pseudo for - * many different transition possibilities. */ -.bg-animate, -.bg-animate:hover, -.bg-animate:focus { - -webkit-transition: background-color .15s ease-in-out; - transition: background-color .15s ease-in-out; -} -/* - - Z-INDEX - - Base - z = z-index - - Modifiers - -0 = literal value 0 - -1 = literal value 1 - -2 = literal value 2 - -3 = literal value 3 - -4 = literal value 4 - -5 = literal value 5 - -999 = literal value 999 - -9999 = literal value 9999 - - -max = largest accepted z-index value as integer - - -inherit = string value inherit - -initial = string value initial - -unset = string value unset - - MDN: https://developer.mozilla.org/en/docs/Web/CSS/z-index - Spec: http://www.w3.org/TR/CSS2/zindex.html - Articles: - https://philipwalton.com/articles/what-no-one-told-you-about-z-index/ - - Tips on extending: - There might be a time worth using negative z-index values. - Or if you are using tachyons with another project, you might need to - adjust these values to suit your needs. - -*/ -.z-0 { z-index: 0; } -.z-1 { z-index: 1; } -.z-2 { z-index: 2; } -.z-3 { z-index: 3; } -.z-4 { z-index: 4; } -.z-5 { z-index: 5; } -.z-999 { z-index: 999; } -.z-9999 { z-index: 9999; } -.z-max { - z-index: 2147483647; -} -.z-inherit { z-index: inherit; } -.z-initial { z-index: auto; z-index: initial; } -.z-unset { z-index: unset; } -/* - - NESTED - Tachyons module for styling nested elements - that are generated by a cms. - -*/ -.nested-copy-line-height p, -.nested-copy-line-height ul, -.nested-copy-line-height ol { - line-height: 1.5; -} -.nested-headline-line-height h1, -.nested-headline-line-height h2, -.nested-headline-line-height h3, -.nested-headline-line-height h4, -.nested-headline-line-height h5, -.nested-headline-line-height h6 { - line-height: 1.25; -} -.nested-list-reset ul, -.nested-list-reset ol { - padding-left: 0; - margin-left: 0; - list-style-type: none; -} -.nested-copy-indent p+p { - text-indent: 1em; - margin-top: 0; - margin-bottom: 0; -} -.nested-copy-separator p+p { - margin-top: 1.5em; -} -.nested-img img { - width: 100%; - max-width: 100%; - display: block; -} -.nested-links a { - color: #0594CB; - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -.nested-links a:hover, -.nested-links a:focus { - color: #96ccff; - -webkit-transition: color .15s ease-in; - transition: color .15s ease-in; -} -/*@import 'tachyons/src/_styles';*/ -/* Variables */ -/* Importing here will allow you to override any variables in the modules */ -/* - - Tachyons - COLOR VARIABLES - - Grayscale - - Solids - - Transparencies - Colors - -*/ -/* - - CUSTOM MEDIA QUERIES - - Media query values can be changed to fit your own content. - There are no magic bullets when it comes to media query width values. - They should be declared in em units - and they should be set to meet - the needs of your content. You can also add additional media queries, - or remove some of the existing ones. - - These media queries can be referenced like so: - - @media (--breakpoint-not-small) { - .medium-and-larger-specific-style { - background-color: red; - } - } - - @media (--breakpoint-medium) { - .medium-screen-specific-style { - background-color: red; - } - } - - @media (--breakpoint-large) { - .large-and-larger-screen-specific-style { - background-color: red; - } - } - -*/ -/* Media Queries */ -/* Debugging */ -/*@import 'tachyons/src/_debug-children'; -@import 'tachyons/src/_debug-grid';*/ -/* Uncomment out the line below to help debug layout issues */ -/* @import 'tachyons/src/_debug'; */ -/* purgecss start ignore */ -.header-link:after { - position: relative; - left: 0.5em; - opacity: 0; - font-size: 0.8em; - -moz-transition: opacity 0.2s ease-in-out 0.1s; - -ms-transition: opacity 0.2s ease-in-out 0.1s; -} -h2:hover .header-link, -h3:hover .header-link, -h4:hover .header-link, -h5:hover .header-link, -h6:hover .header-link { - opacity: 1; -} -.animated { - -webkit-animation-duration: .5s; - animation-duration: .5s; - -webkit-animation-fill-mode: forwards; - animation-fill-mode: forwards; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; -} -@-webkit-keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} -.fadeIn { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; -} -.animated-delay-1 { - -webkit-animation-delay: 0.5s; - animation-delay: 0.5s; -} -.note, -.warning { - - border-left-width: 4px; - border-left-style: solid; - position: relative; - border-color: #0594CB; - - display: block; -} -.note #exclamation-icon, -.warning #exclamation-icon { - - fill: #0594CB; - position: absolute; - top: 35%; - left: -12px; - /*background-color: white;*/ -} -.admonition-content { - display: block; - margin: 0px; - padding: .125em 1em; - /*margin-left: 1em;*/ - margin-top: 2em; - margin-bottom: 2em; - overflow-x: auto; - /*font-size: .9375em;*/ - background-color: rgba(0, 0, 0, .05); - } -.hide-child-menu .child-menu { - display: none; - } -.hide-child-menu:hover .child-menu, - .hide-child-menu:focus .child-menu, - .hide-child-menu:active .child-menu { - display: block; - } -/*documentation-copy headings exaggerate spacing and size to chunk content */ -.documentation-copy h2 { - margin-top: 3em - } -.documentation-copy h2.minor { - font-size: inherit; - margin-top: inherit; - border-bottom: none; -} -.searchbox{display:inline-block;position:relative;width:200px;height:32px!important;white-space:nowrap;-webkit-box-sizing:border-box;box-sizing:border-box;visibility:visible!important} -.searchbox .algolia-autocomplete{display:block;width:100%;height:100%} -.searchbox__wrapper{width:100%;height:100%;z-index:999;position:relative} -.searchbox__input{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:background .4s ease,-webkit-box-shadow .4s ease;transition:background .4s ease,-webkit-box-shadow .4s ease;transition:box-shadow .4s ease,background .4s ease;transition:box-shadow .4s ease,background .4s ease,-webkit-box-shadow .4s ease;border:0;border-radius:16px;-webkit-box-shadow:inset 0 0 0 1px #ccc;box-shadow:inset 0 0 0 1px #ccc;background:#fff!important;padding:0 26px 0 32px;width:100%;height:100%;vertical-align:middle;white-space:normal;font-size:12px;-webkit-appearance:none;-moz-appearance:none;appearance:none} -.searchbox__input::-webkit-search-cancel-button,.searchbox__input::-webkit-search-decoration,.searchbox__input::-webkit-search-results-button,.searchbox__input::-webkit-search-results-decoration{display:none} -.searchbox__input:hover{-webkit-box-shadow:inset 0 0 0 1px #b3b3b3;box-shadow:inset 0 0 0 1px #b3b3b3} -.searchbox__input:active,.searchbox__input:focus{outline:0;-webkit-box-shadow:inset 0 0 0 1px #aaa;box-shadow:inset 0 0 0 1px #aaa;background:#fff} -.searchbox__input::-webkit-input-placeholder{color:#aaa} -.searchbox__input:-ms-input-placeholder{color:#aaa} -.searchbox__input::-ms-input-placeholder{color:#aaa} -.searchbox__input::placeholder{color:#aaa} -.searchbox__submit{position:absolute;top:0;margin:0;border:0;border-radius:16px 0 0 16px;background-color:rgba(69, 142, 225, 0);padding:0;width:32px;height:100%;vertical-align:middle;text-align:center;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;right:inherit;left:0} -.searchbox__submit:before{display:inline-block;margin-right:-4px;height:100%;vertical-align:middle;content:""} -.searchbox__submit:active,.searchbox__submit:hover{cursor:pointer} -.searchbox__submit:focus{outline:0} -.searchbox__submit svg{width:14px;height:14px;vertical-align:middle;fill:#6d7e96} -.searchbox__reset{display:block;position:absolute;top:8px;right:8px;margin:0;border:0;background:none;cursor:pointer;padding:0;font-size:inherit;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;fill:rgba(0, 0, 0, .5)} -.searchbox__reset.hide{display:none} -.searchbox__reset:focus{outline:0} -.searchbox__reset svg{display:block;margin:4px;width:8px;height:8px} -.searchbox__input:valid~.searchbox__reset{display:block;-webkit-animation-name:sbx-reset-in;animation-name:sbx-reset-in;-webkit-animation-duration:.15s;animation-duration:.15s} -@-webkit-keyframes sbx-reset-in{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}} -@keyframes sbx-reset-in{0%{-webkit-transform:translate3d(-20%,0,0);transform:translate3d(-20%,0,0);opacity:0}to{-webkit-transform:none;transform:none;opacity:1}} -.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu{right:0!important;left:inherit!important} -.algolia-autocomplete.algolia-autocomplete-right .ds-dropdown-menu:before{right:48px} -.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu{left:0!important;right:inherit!important} -.algolia-autocomplete.algolia-autocomplete-left .ds-dropdown-menu:before{left:48px} -.algolia-autocomplete .ds-dropdown-menu{top:-6px;border-radius:4px;margin:6px 0 0;padding:0;text-align:left;height:auto;position:relative;background:transparent;border:none;z-index:999;max-width:600px;min-width:500px;-webkit-box-shadow:0 1px 0 0 rgba(0, 0, 0, .2),0 2px 3px 0 rgba(0, 0, 0, .1);box-shadow:0 1px 0 0 rgba(0, 0, 0, .2),0 2px 3px 0 rgba(0, 0, 0, .1)} -.algolia-autocomplete .ds-dropdown-menu:before{display:block;position:absolute;content:"";width:14px;height:14px;background:#fff;z-index:1000;top:-7px;border-top:1px solid #d9d9d9;border-right:1px solid #d9d9d9;-webkit-transform:rotate(-45deg);transform:rotate(-45deg);border-radius:2px} -.algolia-autocomplete .ds-dropdown-menu .ds-suggestions{position:relative;z-index:1000;margin-top:8px} -.algolia-autocomplete .ds-dropdown-menu .ds-suggestions a:hover{text-decoration:none} -.algolia-autocomplete .ds-dropdown-menu .ds-suggestion{cursor:pointer} -.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion.suggestion-layout-simple,.algolia-autocomplete .ds-dropdown-menu .ds-suggestion.ds-cursor .algolia-docsearch-suggestion:not(.suggestion-layout-simple) .algolia-docsearch-suggestion--content{background-color:rgba(69, 142, 225, .05)} -.algolia-autocomplete .ds-dropdown-menu [class^=ds-dataset-]{position:relative;border:1px solid #d9d9d9;background:#fff;border-radius:4px;overflow:auto;padding:0 8px 8px} -.algolia-autocomplete .ds-dropdown-menu *{-webkit-box-sizing:border-box;box-sizing:border-box} -.algolia-autocomplete .algolia-docsearch-suggestion{display:block;position:relative;padding:0 8px;background:#fff;color:#02060c;overflow:hidden} -.algolia-autocomplete .algolia-docsearch-suggestion--highlight{color:#174d8c;background:rgba(143, 187, 237, .1);padding:.1em .05em} -.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl0 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--category-header .algolia-docsearch-suggestion--category-header-lvl1 .algolia-docsearch-suggestion--highlight,.algolia-autocomplete .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{padding:0 0 1px;background:inherit;-webkit-box-shadow:inset 0 -2px 0 0 rgba(69, 142, 225, .8);box-shadow:inset 0 -2px 0 0 rgba(69, 142, 225, .8);color:inherit} -.algolia-autocomplete .algolia-docsearch-suggestion--content{display:block;float:right;width:70%;position:relative;padding:5.33333px 0 5.33333px 10.66667px;cursor:pointer} -.algolia-autocomplete .algolia-docsearch-suggestion--content:before{content:"";position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;left:-1px} -.algolia-autocomplete .algolia-docsearch-suggestion--category-header{position:relative;border-bottom:1px solid #ddd;display:none;margin-top:8px;padding:4px 0;font-size:1em;color:#33363d} -.algolia-autocomplete .algolia-docsearch-suggestion--wrapper{width:100%;float:left;padding:8px 0 0} -.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column{float:left;width:30%;text-align:right;position:relative;padding:5.33333px 10.66667px;color:#a4a7ae;font-size:.9em;word-wrap:break-word} -.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-column:before{content:"";position:absolute;display:block;top:0;height:100%;width:1px;background:#ddd;right:0} -.algolia-autocomplete .algolia-docsearch-suggestion--subcategory-inline{display:none} -.algolia-autocomplete .algolia-docsearch-suggestion--title{margin-bottom:4px;color:#02060c;font-size:.9em;font-weight:700} -.algolia-autocomplete .algolia-docsearch-suggestion--text{display:block;line-height:1.2em;font-size:.85em;color:#63676d} -.algolia-autocomplete .algolia-docsearch-suggestion--no-results{width:100%;padding:8px 0;text-align:center;font-size:1.2em} -.algolia-autocomplete .algolia-docsearch-suggestion--no-results:before{display:none} -.algolia-autocomplete .algolia-docsearch-suggestion code{padding:1px 5px;font-size:90%;border:none;color:#222;background-color:#ebebeb;border-radius:3px;font-family:Menlo,Monaco,Consolas,Courier New,monospace} -.algolia-autocomplete .algolia-docsearch-suggestion code .algolia-docsearch-suggestion--highlight{background:none} -.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__main .algolia-docsearch-suggestion--category-header,.algolia-autocomplete .algolia-docsearch-suggestion.algolia-docsearch-suggestion__secondary{display:block} -@media (min-width:768px){.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column{display:block}} -@media (max-width:768px){.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column{display:inline-block;width:auto;float:left;padding:0;color:#02060c;font-size:.9em;font-weight:700;text-align:left;opacity:.5}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:before{display:none}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--subcategory-column:after{content:"|"}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--content{display:inline-block;width:auto;text-align:left;float:left;padding:0}.algolia-autocomplete .algolia-docsearch-suggestion .algolia-docsearch-suggestion--content:before{display:none}} -.algolia-autocomplete .suggestion-layout-simple.algolia-docsearch-suggestion{border-bottom:1px solid #eee;padding:8px;margin:0} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content{width:100%;padding:0} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--content:before{display:none} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header{margin:0;padding:0;display:block;width:100%;border:none} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl0,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1{opacity:.6;font-size:.85em} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--category-header-lvl1:before{background-image:url('data:image/svg+xml;utf8,');content:"";width:10px;height:10px;display:inline-block} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--wrapper{width:100%;float:left;margin:0;padding:0} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--duplicate-content,.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--subcategory-inline{display:none!important} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title{margin:0;color:#458ee1;font-size:.9em;font-weight:400} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--title:before{content:"#";font-weight:700;color:#458ee1;display:inline-block} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text{margin:4px 0 0;display:block;line-height:1.4em;padding:5.33333px 8px;background:#f8f8f8;font-size:.85em;opacity:.8} -.algolia-autocomplete .suggestion-layout-simple .algolia-docsearch-suggestion--text .algolia-docsearch-suggestion--highlight{color:#3f4145;font-weight:700;-webkit-box-shadow:none;box-shadow:none} -.algolia-autocomplete .algolia-docsearch-footer{width:134px;height:20px;z-index:2000;margin-top:10.66667px;float:right;font-size:0;line-height:0} -.algolia-autocomplete .algolia-docsearch-footer--logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg width='168' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M78.988.938h16.594a2.968 2.968 0 0 1 2.966 2.966V20.5a2.967 2.967 0 0 1-2.966 2.964H78.988a2.967 2.967 0 0 1-2.966-2.964V3.897A2.961 2.961 0 0 1 78.988.938zm41.937 17.866c-4.386.02-4.386-3.54-4.386-4.106l-.007-13.336 2.675-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-10.846-2.18c.821 0 1.43-.047 1.855-.129v-2.719a6.334 6.334 0 0 0-1.574-.199 5.7 5.7 0 0 0-.897.069 2.699 2.699 0 0 0-.814.24c-.24.116-.439.28-.582.491-.15.212-.219.335-.219.656 0 .628.219.991.616 1.23s.938.362 1.615.362zm-.233-9.7c.883 0 1.629.109 2.231.328.602.218 1.088.525 1.444.915.363.396.609.922.76 1.483.157.56.232 1.175.232 1.85v6.874a32.5 32.5 0 0 1-1.868.314c-.834.123-1.772.185-2.813.185-.69 0-1.327-.069-1.895-.198a4.001 4.001 0 0 1-1.471-.636 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.803 0-.656.13-1.073.384-1.525a3.24 3.24 0 0 1 1.047-1.106c.445-.287.95-.492 1.532-.615a8.8 8.8 0 0 1 1.82-.185 8.404 8.404 0 0 1 1.972.24v-.438c0-.307-.035-.6-.11-.874a1.88 1.88 0 0 0-.384-.73 1.784 1.784 0 0 0-.724-.493 3.164 3.164 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.735 7.735 0 0 0-1.26.307l-.321-2.192c.335-.117.834-.233 1.478-.349a10.98 10.98 0 0 1 2.073-.178zm52.842 9.626c.822 0 1.43-.048 1.854-.13V13.7a6.347 6.347 0 0 0-1.574-.199c-.294 0-.595.021-.896.069a2.7 2.7 0 0 0-.814.24 1.46 1.46 0 0 0-.582.491c-.15.212-.218.335-.218.656 0 .628.218.991.615 1.23.404.245.938.362 1.615.362zm-.226-9.694c.883 0 1.629.108 2.231.327.602.219 1.088.526 1.444.915.355.39.609.923.759 1.483a6.8 6.8 0 0 1 .233 1.852v6.873c-.41.088-1.034.19-1.868.314-.834.123-1.772.184-2.813.184-.69 0-1.327-.068-1.895-.198a4.001 4.001 0 0 1-1.471-.635 3.085 3.085 0 0 1-.951-1.134c-.226-.465-.343-1.12-.343-1.804 0-.656.13-1.073.384-1.524.26-.45.608-.82 1.047-1.107.445-.286.95-.491 1.532-.614a8.803 8.803 0 0 1 2.751-.13c.329.034.671.096 1.04.185v-.437a3.3 3.3 0 0 0-.109-.875 1.873 1.873 0 0 0-.384-.731 1.784 1.784 0 0 0-.724-.492 3.165 3.165 0 0 0-1.143-.205c-.616 0-1.177.075-1.69.164a7.75 7.75 0 0 0-1.26.307l-.321-2.193c.335-.116.834-.232 1.478-.348a11.633 11.633 0 0 1 2.073-.177zm-8.034-1.271a1.626 1.626 0 0 1-1.628-1.62c0-.895.725-1.62 1.628-1.62.904 0 1.63.725 1.63 1.62 0 .895-.733 1.62-1.63 1.62zm1.348 13.22h-2.689V7.27l2.69-.423v11.956zm-4.714 0c-4.386.02-4.386-3.54-4.386-4.107l-.008-13.336 2.676-.424v13.254c0 .322 0 2.358 1.718 2.364v2.248zm-8.698-5.903c0-1.156-.253-2.119-.746-2.788-.493-.677-1.183-1.01-2.067-1.01-.882 0-1.574.333-2.065 1.01-.493.676-.733 1.632-.733 2.788 0 1.168.246 1.953.74 2.63.492.683 1.183 1.018 2.066 1.018.882 0 1.574-.342 2.067-1.019.492-.683.738-1.46.738-2.63zm2.737-.007c0 .902-.13 1.584-.397 2.33a5.52 5.52 0 0 1-1.128 1.906 4.986 4.986 0 0 1-1.752 1.223c-.685.286-1.739.45-2.265.45-.528-.006-1.574-.157-2.252-.45a5.096 5.096 0 0 1-1.744-1.223c-.487-.527-.863-1.162-1.137-1.906a6.345 6.345 0 0 1-.41-2.33c0-.902.123-1.77.397-2.508a5.554 5.554 0 0 1 1.15-1.892 5.133 5.133 0 0 1 1.75-1.216c.679-.287 1.425-.423 2.232-.423.808 0 1.553.142 2.237.423a4.88 4.88 0 0 1 1.753 1.216 5.644 5.644 0 0 1 1.135 1.892c.287.738.431 1.606.431 2.508zm-20.138 0c0 1.12.246 2.363.738 2.882.493.52 1.13.78 1.91.78.424 0 .828-.062 1.204-.178.377-.116.677-.253.917-.417V9.33a10.476 10.476 0 0 0-1.766-.226c-.971-.028-1.71.37-2.23 1.004-.513.636-.773 1.75-.773 2.788zm7.438 5.274c0 1.824-.466 3.156-1.404 4.004-.936.846-2.367 1.27-4.296 1.27-.705 0-2.17-.137-3.34-.396l.431-2.118c.98.205 2.272.26 2.95.26 1.074 0 1.84-.219 2.299-.656.459-.437.684-1.086.684-1.948v-.437a8.07 8.07 0 0 1-1.047.397c-.43.13-.93.198-1.492.198-.739 0-1.41-.116-2.018-.349a4.206 4.206 0 0 1-1.567-1.025c-.431-.45-.774-1.017-1.013-1.694-.24-.677-.363-1.885-.363-2.773 0-.834.13-1.88.384-2.577.26-.696.629-1.298 1.129-1.796.493-.498 1.095-.881 1.8-1.162a6.605 6.605 0 0 1 2.428-.457c.87 0 1.67.109 2.45.24.78.129 1.444.265 1.985.415V18.17z' fill='%235468FF'/%3E%3Cpath d='M6.972 6.677v1.627c-.712-.446-1.52-.67-2.425-.67-.585 0-1.045.13-1.38.391a1.24 1.24 0 0 0-.502 1.03c0 .425.164.765.494 1.02.33.256.835.532 1.516.83.447.192.795.356 1.045.495.25.138.537.332.862.582.324.25.563.548.718.894.154.345.23.741.23 1.188 0 .947-.334 1.691-1.004 2.234-.67.542-1.537.814-2.601.814-1.18 0-2.16-.229-2.936-.686v-1.708c.84.628 1.814.942 2.92.942.585 0 1.048-.136 1.388-.407.34-.271.51-.646.51-1.125 0-.287-.1-.55-.302-.79-.203-.24-.42-.42-.655-.542-.234-.123-.585-.29-1.053-.503a61.27 61.27 0 0 1-.582-.271 13.67 13.67 0 0 1-.55-.287 4.275 4.275 0 0 1-.567-.351 6.92 6.92 0 0 1-.455-.4c-.18-.17-.31-.34-.39-.51-.08-.17-.155-.37-.224-.598a2.553 2.553 0 0 1-.104-.742c0-.915.333-1.638.998-2.17.664-.532 1.523-.798 2.576-.798.968 0 1.793.17 2.473.51zm7.468 5.696v-.287c-.022-.607-.187-1.088-.495-1.444-.309-.357-.75-.535-1.324-.535-.532 0-.99.194-1.373.583-.382.388-.622.949-.717 1.683h3.909zm1.005 2.792v1.404c-.596.34-1.383.51-2.362.51-1.255 0-2.255-.377-3-1.132-.744-.755-1.116-1.744-1.116-2.968 0-1.297.34-2.316 1.021-3.055.68-.74 1.548-1.11 2.6-1.11 1.033 0 1.852.323 2.458.966.606.644.91 1.572.91 2.784 0 .33-.033.676-.096 1.038h-5.314c.107.702.405 1.239.894 1.611.49.372 1.106.558 1.85.558.862 0 1.58-.202 2.155-.606zm6.605-1.77h-1.212c-.596 0-1.045.116-1.349.35-.303.234-.454.532-.454.894 0 .372.117.664.35.877.235.213.575.32 1.022.32.51 0 .912-.142 1.204-.424.293-.281.44-.651.44-1.108v-.91zm-4.068-2.554V9.325c.627-.361 1.457-.542 2.489-.542 2.116 0 3.175 1.026 3.175 3.08V17h-1.548v-.957c-.415.68-1.143 1.02-2.186 1.02-.766 0-1.38-.22-1.843-.661-.462-.442-.694-1.003-.694-1.684 0-.776.293-1.38.878-1.81.585-.431 1.404-.647 2.457-.647h1.34V11.8c0-.554-.133-.971-.399-1.253-.266-.282-.707-.423-1.324-.423a4.07 4.07 0 0 0-2.345.718zm9.333-1.93v1.42c.394-1 1.101-1.5 2.123-1.5.148 0 .313.016.494.048v1.531a1.885 1.885 0 0 0-.75-.143c-.542 0-.989.24-1.34.718-.351.479-.527 1.048-.527 1.707V17h-1.563V8.91h1.563zm5.01 4.084c.022.82.272 1.492.75 2.019.479.526 1.15.79 2.01.79.639 0 1.235-.176 1.788-.527v1.404c-.521.319-1.186.479-1.995.479-1.265 0-2.276-.4-3.031-1.197-.755-.798-1.133-1.792-1.133-2.984 0-1.16.38-2.151 1.14-2.975.761-.825 1.79-1.237 3.088-1.237.702 0 1.346.149 1.93.447v1.436a3.242 3.242 0 0 0-1.77-.495c-.84 0-1.513.266-2.019.798-.505.532-.758 1.213-.758 2.042zM40.24 5.72v4.579c.458-1 1.293-1.5 2.505-1.5.787 0 1.42.245 1.899.734.479.49.718 1.17.718 2.042V17h-1.564v-5.106c0-.553-.14-.98-.422-1.284-.282-.303-.652-.455-1.11-.455-.531 0-1.002.202-1.411.606-.41.405-.615 1.022-.615 1.851V17h-1.563V5.72h1.563zm14.966 10.02c.596 0 1.096-.253 1.5-.758.404-.506.606-1.157.606-1.955 0-.915-.202-1.62-.606-2.114-.404-.495-.92-.742-1.548-.742-.553 0-1.05.224-1.491.67-.442.447-.662 1.133-.662 2.058 0 .958.212 1.67.638 2.138.425.469.946.703 1.563.703zM53.004 5.72v4.42c.574-.894 1.388-1.341 2.44-1.341 1.022 0 1.857.383 2.506 1.149.649.766.973 1.781.973 3.047 0 1.138-.309 2.109-.925 2.912-.617.803-1.463 1.205-2.537 1.205-1.075 0-1.894-.447-2.457-1.34V17h-1.58V5.72h1.58zm9.908 11.104l-3.223-7.913h1.739l1.005 2.632 1.26 3.415c.096-.32.48-1.458 1.15-3.415l.909-2.632h1.66l-2.92 7.866c-.777 2.074-1.963 3.11-3.559 3.11a2.92 2.92 0 0 1-.734-.079v-1.34c.17.042.351.064.543.064 1.032 0 1.755-.57 2.17-1.708z' fill='%235D6494'/%3E%3Cpath d='M89.632 5.967v-.772a.978.978 0 0 0-.978-.977h-2.28a.978.978 0 0 0-.978.977v.793c0 .088.082.15.171.13a7.127 7.127 0 0 1 1.984-.28c.65 0 1.295.088 1.917.259.082.02.164-.04.164-.13m-6.248 1.01l-.39-.389a.977.977 0 0 0-1.382 0l-.465.465a.973.973 0 0 0 0 1.38l.383.383c.062.061.15.047.205-.014.226-.307.472-.601.746-.874.281-.28.568-.526.883-.751.068-.042.075-.137.02-.2m4.16 2.453v3.341c0 .096.104.165.192.117l2.97-1.537c.068-.034.089-.117.055-.184a3.695 3.695 0 0 0-3.08-1.866c-.068 0-.136.054-.136.13m0 8.048a4.489 4.489 0 0 1-4.49-4.482 4.488 4.488 0 0 1 4.49-4.482 4.488 4.488 0 0 1 4.489 4.482 4.484 4.484 0 0 1-4.49 4.482m0-10.85a6.363 6.363 0 1 0 0 12.729 6.37 6.37 0 0 0 6.372-6.368 6.358 6.358 0 0 0-6.371-6.36' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;background-position:50%;background-size:100%;overflow:hidden;text-indent:-9000px;padding:0!important;width:100%;height:100%;display:block} -/* These styles enhance the home page carousel, located here: themes/gohugoioTheme/layouts/partials/home-page-sections/showcase.html */ -.overflow-x-scroll{ - -webkit-overflow-scrolling: touch; -} -.row { - -webkit-transition: 450ms -webkit-transform; - transition: 450ms -webkit-transform; - transition: 450ms transform; - transition: 450ms transform, 450ms -webkit-transform; - font-size: 0; -} -.tile { - -webkit-transition: 450ms all; - transition: 450ms all; -} -.details { - background: -webkit-gradient(linear, left bottom, left top, from(rgba(0, 0, 0, .9)), to(rgba(0, 0, 0, 0))); - background: linear-gradient(to top, rgba(0, 0, 0, .9) 0%, rgba(0, 0, 0, 0) 100%); - -webkit-transition: 450ms opacity; - transition: 450ms opacity; -} -.tile:hover .details { - opacity: 1; -} -.row:hover .tile { - opacity: 0.3; -} -.row:hover .tile:hover { - opacity: 1; -} -.chroma .lntable pre { - padding: 0; - margin: 0; - border: 0; -} -.chroma .lntable pre code { - padding: 0; - margin: 0; -} -code { - padding: 0.2em; - margin: 0; - font-size: 85%; - background-color: rgba(27, 31, 35, .05); - border-radius: 3px; -} -pre code { - display: block; - padding: 1.5em 1.5em; - font-size: .875rem; - line-height: 2; - overflow-x: auto; -} -pre { - background-color: #fff; - color: #333; - white-space: pre; - -webkit-hyphens: none; - -ms-hyphens: none; - hyphens: none; - position: relative; - border-width: 1px; - border-color: #ccc; - border-style: solid; -} -/* The Pygments highlighter comes with its own styles. */ -.highlight pre { - background-color: inherit; - color: inherit; - padding: 0.5em; - font-size: .875rem; -} -/*We are adding the copy button content here so we can change it with javascript. See the "Clipboard scripts"*/ -.copy:after { - content: "Copy" -} -.copied:after { - content: "Copied" -} -@media screen and (min-width: 60em) { - .full-width - { - /*width: 100vw; - position: relative; - left: 50%; - right: 50%; - margin-left: -50vw; - margin-right: -50vw;*/ - /*width: 60vw;*/ - /*position: relative; - left: 50%; - right: 50%;*/ - /*margin-left: -30vw;*/ - margin-right: -30vw; - max-width: 100vw; - } -} -.code-block .line-numbers-rows { - background: #2f3a46; - border: none; - bottom: -50px; - color: #98a4b3; - left: -178px; - padding: 50px 0; - top: -50px; - width: 138px -} -.code-block .line-numbers-rows>span:before { - color: inherit; - padding-right: 30px -} -.tab-button{ - margin-bottom:1px; - position: relative; - z-index: 1; - color:#333; - border-color:#ccc; - outline: none; - background-color:white; -} -.tab-pane code{ - background:#f1f2f2; - border-radius:0; -} -.tab-pane .chroma{ - background:none; - padding:0; -} -.tab-button.active{ - border-bottom-color:#f1f2f2; - background-color: #f1f2f2; -} -.tab-content .tab-pane{ - display: none; -} -.tab-content .tab-pane.active{ - display: block; -} -/* Treatment of copy buttons inside a tab module */ -.tab-content .copy, .tab-content .copied{ - display: none; -} -.tab-content .tab-pane.active + .copy, .tab-content .tab-pane.active + .copied{ - display: block; -} -.primary-color {color: #0594CB} -.bg-primary-color {background-color: #0594CB} -.hover-bg-primary-color:hover {background-color: #0594CB} -.primary-color-dark {color: #0A1922} -.bg-primary-color-dark {background-color: #0A1922} -.hover-bg-primary-color-dark:hover {background-color: #0A1922} -.primary-color-light {color: #f9f9f9} -.bg-primary-color-light {background-color: #f9f9f9} -.hover-bg-primary-color-light:hover {background-color: #f9f9f9} -.accent-color {color: #EBB951} -.bg-accent-color {background-color: #EBB951} -.hover-bg-accent-color:hover {background-color: #EBB951} -.accent-color-light {color: #FF4088} -.hover-accent-color-light:hover {color: #FF4088} -.bg-accent-color-light {background-color: #FF4088} -.hover-bg-accent-color-light:hover {background-color: #FF4088} -.accent-color-dark {color: #33ba91} -.bg-accent-color-dark {background-color: #33ba91} -.hover-bg-accent-color-dark:hover {background-color: #33ba91} -.text-color-primary {color: #373737} -.text-on-primary-color {color: #fff} -.text-color-secondary {color: #ccc} -.text-color-disabled {color: #F7f7f7} -.divider-color {color: #f6f6f6} -.warn-color {color: red} -.nested-links a { - color: #0594CB; - text-decoration: none; - -} -.column-count-2 {-webkit-column-count: 1;column-count: 1} -.column-gap-1 {-webkit-column-gap: 0;column-gap: 0} -.break-inside-avoid {-webkit-column-break-inside: auto;break-inside: auto} -@media screen and (min-width: 60em) { - .column-count-3-l {-webkit-column-count: 3;column-count: 3} - .column-count-2-l {-webkit-column-count: 2;column-count: 2} - .column-gap-1-l {-webkit-column-gap: 1;column-gap: 1} - .break-inside-avoid-l {-webkit-column-break-inside: avoid;break-inside: avoid} -} -.prose ul, .prose ol { - margin-bottom: 2em; -} -.prose ul li, .prose ol li { - margin-bottom: .5em; -} -.prose li:hover { - background-color: #eee -} -.prose ::selection { - background: #0594CB; /* WebKit/Blink Browsers */ - color: white; -} -body { - -line-height: 1.45; - -} -p {margin-bottom: 1.3em;} -h1, h2, h3, h4 { -margin: 1.414em 0 0.5em; - -line-height: 1.2; -} -h1 { -margin-top: 0; -font-size: 2.441em; -} -h2 {font-size: 1.953em;} -h3 {font-size: 1.563em;} -h4 {font-size: 1.25em;} -small, .font_small {font-size: 0.8em;} -.prose table { - width: 100%; - margin-bottom: 3em; - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; - border: 1px solid #eee - -} -.prose table th { - background-color: #0594CB; - border-bottom: 1px solid #0594CB; - color: white; - font-weight: 400; - text-align: left; - padding: .375em .5em; -} -.prose table td, .prose table tc { - padding: .75em .5em; - text-align: left; - border-right: 1px solid #eee; -} -.prose table tr:nth-child(even) { - background-color: #eee; -} -dl dt { - font-weight: bold; - font-size: 1.125rem; -} -dd { - margin: .5em 0 2em 0; - padding: 0; -} -.f2-fluid { - font-size: 2.25rem; -} -@media screen and (min-width: 60em) { - .f2-fluid { - font-size: 1.25rem; - font-size: calc(0.70833rem + 0.83333vw); - } -} -/* From https://www.cssfontstack.com */ -code, .code, pre code, .highlight pre { - font-family: 'inconsolata',Menlo,Monaco,'Courier New',monospace; -} -.sans-serif { - font-family: 'Muli', - avenir, - 'helvetica neue', helvetica, - ubuntu, - roboto, noto, - 'segoe ui', arial, - sans-serif; -} -.serif { - font-family: Palatino,"Palatino Linotype","Palatino LT STD","Book Antiqua",Georgia,serif; -} -/* Monospaced Typefaces (for code) */ -.courier { - font-family: 'Courier Next', - courier, - monospace; -} -/* Sans-Serif Typefaces */ -.helvetica { - font-family: 'helvetica neue', helvetica, - sans-serif; -} -.avenir { - font-family: 'avenir next', avenir, - sans-serif; -} -/* Serif Typefaces */ -.athelas { - font-family: athelas, - georgia, - serif; -} -.georgia { - font-family: georgia, - serif; -} -.times { - font-family: times, - serif; -} -.bodoni { - font-family: "Bodoni MT", - serif; -} -.calisto { - font-family: "Calisto MT", - serif; -} -.garamond { - font-family: garamond, - serif; -} -.baskerville { - font-family: baskerville, - serif; -} -/* pagination.html: https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/template_embedded.go#L117 */ -.pagination { - margin: 3rem 0; -} -.pagination li { - display: inline-block; - margin-right: .375rem; - font-size: .875rem; - margin-bottom: 2.5em; -} -.pagination li a { - padding: .5rem .625rem; - background-color: white; - color: #333; - border: 1px solid #ddd; - border-radius: 3px; - text-decoration: none; -} -.pagination li.disabled { - display: none; -} -.pagination li.active a:link, -.pagination li.active a:active, -.pagination li.active a:visited { - background-color: #ddd; -} -/* Hides non-meaningful TOC items*/ -#TableOfContents ul li ul li ul li{ - display: none; - } -#TableOfContents ul li { - color: black; - display: block; - margin-bottom: .375em; - line-height: 1.375; -} -#TableOfContents ul li a{ - width: 100%; - padding: .25em .375em; - margin-left: -.375em; - -} -#TableOfContents ul li a:hover { - background-color: #999; - color: white; - -} -.no-js .needs-js { - opacity: 0 -} -.js .needs-js { - opacity: 1; - -webkit-transition: opacity .15s ease-in; - transition: opacity .15s ease-in; -} -.facebook, .twitter, .instagram, .youtube { - fill: #BABABA; -} -.facebook:hover { - fill: #3b5998; -} -.twitter { - fill: #55acee; -} -.twitter:hover { - fill: #BABABA; -} -.instagram:hover { - fill: #e95950; -} -.youtube:hover { - fill: #bb0000; -} -@media (min-width: 75em) { - - [data-scrolldir="down"] .sticky { - position: fixed; - top:100px; - right:0; - } - - [data-scrolldir="up"] .sticky { - position: fixed; - top:100px; - right:0; - } -} -.fill-current { fill: currentColor; } -/* Background */ -.chroma { background-color: #ffffff } -/* Error */ -.chroma .err { color: #a61717; background-color: #e3d2d2 } -/* LineTableTD */ -.chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } -/* LineTable */ -.chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } -/* LineHighlight */ -.chroma .hl { display: block; width: 100%;background-color: #ffffcc } -/* LineNumbersTable */ -.chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } -/* LineNumbers */ -.chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; } -/* Keyword */ -.chroma .k { font-weight: bold } -/* KeywordConstant */ -.chroma .kc { font-weight: bold } -/* KeywordDeclaration */ -.chroma .kd { font-weight: bold } -/* KeywordNamespace */ -.chroma .kn { font-weight: bold } -/* KeywordPseudo */ -.chroma .kp { font-weight: bold } -/* KeywordReserved */ -.chroma .kr { font-weight: bold } -/* KeywordType */ -.chroma .kt { color: #445588; font-weight: bold } -/* NameAttribute */ -.chroma .na { color: #008080 } -/* NameBuiltin */ -.chroma .nb { color: #999999 } -/* NameClass */ -.chroma .nc { color: #445588; font-weight: bold } -/* NameConstant */ -.chroma .no { color: #008080 } -/* NameEntity */ -.chroma .ni { color: #800080 } -/* NameException */ -.chroma .ne { color: #990000; font-weight: bold } -/* NameFunction */ -.chroma .nf { color: #990000; font-weight: bold } -/* NameNamespace */ -.chroma .nn { color: #555555 } -/* NameTag */ -.chroma .nt { color: #000080 } -/* NameVariable */ -.chroma .nv { color: #008080 } -/* LiteralString */ -.chroma .s { color: #bb8844 } -/* LiteralStringAffix */ -.chroma .sa { color: #bb8844 } -/* LiteralStringBacktick */ -.chroma .sb { color: #bb8844 } -/* LiteralStringChar */ -.chroma .sc { color: #bb8844 } -/* LiteralStringDelimiter */ -.chroma .dl { color: #bb8844 } -/* LiteralStringDoc */ -.chroma .sd { color: #bb8844 } -/* LiteralStringDouble */ -.chroma .s2 { color: #bb8844 } -/* LiteralStringEscape */ -.chroma .se { color: #bb8844 } -/* LiteralStringHeredoc */ -.chroma .sh { color: #bb8844 } -/* LiteralStringInterpol */ -.chroma .si { color: #bb8844 } -/* LiteralStringOther */ -.chroma .sx { color: #bb8844 } -/* LiteralStringRegex */ -.chroma .sr { color: #808000 } -/* LiteralStringSingle */ -.chroma .s1 { color: #bb8844 } -/* LiteralStringSymbol */ -.chroma .ss { color: #bb8844 } -/* LiteralNumber */ -.chroma .m { color: #009999 } -/* LiteralNumberBin */ -.chroma .mb { color: #009999 } -/* LiteralNumberFloat */ -.chroma .mf { color: #009999 } -/* LiteralNumberHex */ -.chroma .mh { color: #009999 } -/* LiteralNumberInteger */ -.chroma .mi { color: #009999 } -/* LiteralNumberIntegerLong */ -.chroma .il { color: #009999 } -/* LiteralNumberOct */ -.chroma .mo { color: #009999 } -/* Operator */ -.chroma .o { font-weight: bold } -/* OperatorWord */ -.chroma .ow { font-weight: bold } -/* Comment */ -.chroma .c { color: #999988; font-style: italic } -/* CommentHashbang */ -.chroma .ch { color: #999988; font-style: italic } -/* CommentMultiline */ -.chroma .cm { color: #999988; font-style: italic } -/* CommentSingle */ -.chroma .c1 { color: #999988; font-style: italic } -/* CommentSpecial */ -.chroma .cs { color: #999999; font-weight: bold; font-style: italic } -/* CommentPreproc */ -.chroma .cp { color: #999999; font-weight: bold } -/* CommentPreprocFile */ -.chroma .cpf { color: #999999; font-weight: bold } -/* GenericDeleted */ -.chroma .gd { color: #000000; background-color: #ffdddd } -/* GenericEmph */ -.chroma .ge { font-style: italic } -/* GenericError */ -.chroma .gr { color: #aa0000 } -/* GenericHeading */ -.chroma .gh { color: #999999 } -/* GenericInserted */ -.chroma .gi { color: #000000; background-color: #ddffdd } -/* GenericOutput */ -.chroma .go { color: #888888 } -/* GenericPrompt */ -.chroma .gp { color: #555555 } -/* GenericStrong */ -.chroma .gs { font-weight: bold } -/* GenericSubheading */ -.chroma .gu { color: #aaaaaa } -/* GenericTraceback */ -.chroma .gt { color: #aa0000 } -/* TextWhitespace */ -.chroma .w { color: #bbbbbb } -.nested-blockquote blockquote { - border-left: 4px solid #0594CB; - padding-left: 1em; - /*margin: 0;*/ -} -.mw-90 { - max-width:90%; -} -/* purgecss end ignore */ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/js/app.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/js/app.js deleted file mode 100644 index a3e1801f8..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/assets/output/js/app.js +++ /dev/null @@ -1,17 +0,0 @@ -!function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([function(t,e,n){!function(e,n){var r=function(t,e,n){"use strict";var r,i;if(function(){var e,n={lazyClass:"lazyload",loadedClass:"lazyloaded",loadingClass:"lazyloading",preloadClass:"lazypreload",errorClass:"lazyerror",autosizesClass:"lazyautosizes",fastLoadedClass:"ls-is-cached",iframeLoadMode:0,srcAttr:"data-src",srcsetAttr:"data-srcset",sizesAttr:"data-sizes",minSize:40,customMedia:{},init:!0,expFactor:1.5,hFac:.8,loadMode:2,loadHidden:!0,ricTimeout:0,throttleDelay:125};for(e in i=t.lazySizesConfig||t.lazysizesConfig||{},n)e in i||(i[e]=n[e])}(),!e||!e.getElementsByClassName)return{init:function(){},cfg:i,noSupport:!0};var o=e.documentElement,s=t.HTMLPictureElement,a=t.addEventListener.bind(t),u=t.setTimeout,c=t.requestAnimationFrame||u,l=t.requestIdleCallback,h=/^picture$/i,f=["load","error","lazyincluded","_lazyloaded"],d={},p=Array.prototype.forEach,g=function(t,e){return d[e]||(d[e]=new RegExp("(\\s|^)"+e+"(\\s|$)")),d[e].test(t.getAttribute("class")||"")&&d[e]},m=function(t,e){g(t,e)||t.setAttribute("class",(t.getAttribute("class")||"").trim()+" "+e)},y=function(t,e){var n;(n=g(t,e))&&t.setAttribute("class",(t.getAttribute("class")||"").replace(n," "))},v=function(t,e,n){var r=n?"addEventListener":"removeEventListener";n&&v(t,e),f.forEach((function(n){t[r](n,e)}))},b=function(t,n,i,o,s){var a=e.createEvent("Event");return i||(i={}),i.instance=r,a.initEvent(n,!o,!s),a.detail=i,t.dispatchEvent(a),a},w=function(e,n){var r;!s&&(r=t.picturefill||i.pf)?(n&&n.src&&!e.getAttribute("srcset")&&e.setAttribute("srcset",n.src),r({reevaluate:!0,elements:[e]})):n&&n.src&&(e.src=n.src)},_=function(t,e){return(getComputedStyle(t,null)||{})[e]},x=function(t,e,n){for(n=n||t.offsetWidth;n0)&&"visible"!=_(i,"overflow")&&(r=i.getBoundingClientRect(),s=z>r.left&&Fr.top-1&&H500&&o.clientWidth>500?500:370:i.expand,r._defEx=d,p=d*i.expFactor,g=i.hFac,U=null,W2&&D>2&&!e.hidden?(W=p,X=0):W=D>1&&X>1&&Q<6?d:0),f!==c&&($=innerWidth+c*g,M=innerHeight+c,l=-1*c,f=c),s=m[n].getBoundingClientRect(),(B=s.bottom)>=l&&(H=s.top)<=M&&(z=s.right)>=l*g&&(F=s.left)<=$&&(B||z||F||H)&&(i.loadHidden||Z(m[n]))&&(R&&Q<3&&!h&&(D<3||X<4)||Y(m[n],c))){if(at(m[n]),u=!0,Q>9)break}else!u&&R&&!a&&Q<4&&X<4&&D>2&&(L[0]||i.preloadAfterLoad)&&(L[0]||!h&&(B||z||F||H||"auto"!=m[n].getAttribute(i.sizesAttr)))&&(a=L[0]||m[n]);a&&!u&&at(a)}},et=function(t){var e,r=0,o=i.throttleDelay,s=i.ricTimeout,a=function(){e=!1,r=n.now(),t()},c=l&&s>49?function(){l(a,{timeout:s}),s!==i.ricTimeout&&(s=i.ricTimeout)}:C((function(){u(a)}),!0);return function(t){var i;(t=!0===t)&&(s=33),e||(e=!0,(i=o-(n.now()-r))<0&&(i=0),t||i<9?c():u(c,i))}}(tt),nt=function(t){var e=t.target;e._lazyCache?delete e._lazyCache:(G(t),m(e,i.loadedClass),y(e,i.loadingClass),v(e,it),b(e,"lazyloaded"))},rt=C(nt),it=function(t){rt({target:t.target})},ot=function(t){var e,n=t.getAttribute(i.srcsetAttr);(e=i.customMedia[t.getAttribute("data-media")||t.getAttribute("media")])&&t.setAttribute("media",e),n&&t.setAttribute("srcset",n)},st=C((function(t,e,n,r,o){var s,a,c,l,f,d;(f=b(t,"lazybeforeunveil",e)).defaultPrevented||(r&&(n?m(t,i.autosizesClass):t.setAttribute("sizes",r)),a=t.getAttribute(i.srcsetAttr),s=t.getAttribute(i.srcAttr),o&&(l=(c=t.parentNode)&&h.test(c.nodeName||"")),d=e.firesLoad||"src"in t&&(a||s||l),f={target:t},m(t,i.loadingClass),d&&(clearTimeout(P),P=u(G,2500),v(t,it,!0)),l&&p.call(c.getElementsByTagName("source"),ot),a?t.setAttribute("srcset",a):s&&!l&&(K.test(t.nodeName)?function(t,e){var n=t.getAttribute("data-load-mode")||i.iframeLoadMode;0==n?t.contentWindow.location.replace(e):1==n&&(t.src=e)}(t,s):t.src=s),o&&(a||l)&&w(t,{src:s})),t._lazyRace&&delete t._lazyRace,y(t,i.lazyClass),S((function(){var e=t.complete&&t.naturalWidth>1;d&&!e||(e&&m(t,i.fastLoadedClass),nt(f),t._lazyCache=!0,u((function(){"_lazyCache"in t&&delete t._lazyCache}),9)),"lazy"==t.loading&&Q--}),!0)})),at=function(t){if(!t._lazyRace){var e,n=V.test(t.nodeName),r=n&&(t.getAttribute(i.sizesAttr)||t.getAttribute("sizes")),o="auto"==r;(!o&&R||!n||!t.getAttribute("src")&&!t.srcset||t.complete||g(t,i.errorClass)||!g(t,i.lazyClass))&&(e=b(t,"lazyunveilread").detail,o&&T.updateElem(t,!0,t.offsetWidth),t._lazyRace=!0,Q++,st(t,e,o,r,n))}},ut=A((function(){i.loadMode=3,et()})),ct=function(){3==i.loadMode&&(i.loadMode=2),ut()},lt=function(){R||(n.now()-q<999?u(lt,999):(R=!0,i.loadMode=3,et(),a("scroll",ct,!0)))},{_:function(){q=n.now(),r.elements=e.getElementsByClassName(i.lazyClass),L=e.getElementsByClassName(i.lazyClass+" "+i.preloadClass),a("scroll",et,!0),a("resize",et,!0),a("pageshow",(function(t){if(t.persisted){var n=e.querySelectorAll("."+i.loadingClass);n.length&&n.forEach&&c((function(){n.forEach((function(t){t.complete&&at(t)}))}))}})),t.MutationObserver?new MutationObserver(et).observe(o,{childList:!0,subtree:!0,attributes:!0}):(o.addEventListener("DOMNodeInserted",et,!0),o.addEventListener("DOMAttrModified",et,!0),setInterval(et,999)),a("hashchange",et,!0),["focus","mouseover","click","load","transitionend","animationend"].forEach((function(t){e.addEventListener(t,et,!0)})),/d$|^c/.test(e.readyState)?lt():(a("load",lt),e.addEventListener("DOMContentLoaded",et),u(lt,2e4)),r.elements.length?(tt(),S._lsFlush()):et()},checkElems:et,unveil:at,_aLSL:ct}),T=(N=C((function(t,e,n,r){var i,o,s;if(t._lazysizesWidth=r,r+="px",t.setAttribute("sizes",r),h.test(e.nodeName||""))for(o=0,s=(i=e.getElementsByTagName("source")).length;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===r(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,a.default)(t,"click",(function(t){return e.onClick(t)}))}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new o.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return l("action",t)}},{key:"defaultTarget",value:function(t){var e=l("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return l("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach((function(t){n=n&&!!document.queryCommandSupported(t)})),n}}]),e}(s.default);function l(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}t.exports=c},function(t,e,n){"use strict";var r,i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,a.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,a.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=u},function(t,e){t.exports=function(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var r=window.getSelection(),i=document.createRange();i.selectNodeContents(t),r.removeAllRanges(),r.addRange(i),e=r.toString()}return e}},function(t,e){function n(){}n.prototype={on:function(t,e,n){var r=this.e||(this.e={});return(r[t]||(r[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){var r=this;function i(){r.off(t,i),e.apply(n,arguments)}return i._=e,this.on(t,i,n)},emit:function(t){for(var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),r=0,i=n.length;r";var r=document.createElement("div");r.appendChild(document.createTextNode(e)),n=n||"";var i=document.createElement("div");i.appendChild(document.createTextNode(n));var s=document.createElement("div");return s.appendChild(document.createTextNode(t)),s.innerHTML.replace(RegExp(o(r.innerHTML),"g"),e).replace(RegExp(o(i.innerHTML),"g"),n)}}},function(t,e,n){"use strict";t.exports={element:null}},function(t,e){var n=Object.prototype.hasOwnProperty,r=Object.prototype.toString;t.exports=function(t,e,i){if("[object Function]"!==r.call(e))throw new TypeError("iterator must be a function");var o=t.length;if(o===+o)for(var s=0;s was loaded but did not call our provided callback"),JSONPScriptError:o("JSONPScriptError"," - -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/head-additions.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/head-additions.html deleted file mode 100644 index af615ee7c..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/head-additions.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hero.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hero.html deleted file mode 100644 index 9e7240433..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hero.html +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html deleted file mode 100644 index a7733acdc..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-icons.html +++ /dev/null @@ -1,34 +0,0 @@ -
    - {{ if .Params.features }} -
    - {{ $features := .Params.features }} - {{ range $i, $e := $features }} - {{ $features_count := $e | len }} - -
    - -
    -
    - {{ with .image_path }} - icon depicting {{ $e.heading }} - {{ end }} -
    - -
    -

    - {{ .heading }} -

    -
    -

    {{.tagline}}

    -
    - {{ .copy }} -
    -
    -
    -
    - -
    - {{ end }} -
    - {{ end }} -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-single.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-single.html deleted file mode 100644 index f36b3d674..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/features-single.html +++ /dev/null @@ -1,32 +0,0 @@ -{{ if .Params.sections }} - {{ range .Params.sections }} - {{ $.Scratch.Add "i" 1 }}{{ $i := $.Scratch.Get "i" }} - -
    -
    - -
    -
    - image depicting an example of {{ .heading }} -
    -
    - - - -
    -
    - - {{ end }} -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/installation.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/installation.html deleted file mode 100644 index cf2989ddb..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/installation.html +++ /dev/null @@ -1,38 +0,0 @@ -
    - - -
    -
    -

    Install in seconds, build in milliseconds.

    -

    Hugo works on macOS, Windows, Linux, FreeBSD, and others.

    -

    Host on any server or your favorite CDN.

    -
    -
    - - -
    -
    - Hugo Gopher -
    -

    macOS

    -
    - $ brew install hugo
    -
    -

    Windows

    -
    - $ choco install hugo-extended
    -
    -

    Linux

    -
    - $ sudo snap install hugo
    -
    - - - -
    - - - - - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html deleted file mode 100644 index 5300fb7a8..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/open-source-involvement.html +++ /dev/null @@ -1,59 +0,0 @@ -
    -
    - Github Logo -
    -
    - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/showcase.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/showcase.html deleted file mode 100644 index c73cfa5e9..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/showcase.html +++ /dev/null @@ -1,44 +0,0 @@ -
    -

    Showcase

    - {{/* NOTE: transitions for this section are in themes/gohugoioTheme/src/css/_carousel.css */}} -
    -
    -
    - {{ $showcasePages := where .Site.RegularPages "Section" "showcase" }} - {{ if $showcasePages }} - {{ template "home_showcase_item" (index $showcasePages 0) }} - {{ range $p := first 10 ($showcasePages | after 1 | shuffle) }} - {{template "home_showcase_item" $p }} - {{end}} - {{end}} -
    -
    -
    - {{/* END */}} -
    {{/* using Flex to make the button show up on the right side */}} - See All -
    -
    - - -{{ define "home_showcase_item" }} - {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }} - {{ with $img }} - {{ $big := .Fill "1024x512 top" }} - {{ $small := $big.Resize "512x" }} - - {{with $.Title}} -
    -
    - {{.}} → -
    -
    - {{end}} -
    - {{ end }} -{{ end }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html deleted file mode 100644 index 32bc44f6a..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/sponsors.html +++ /dev/null @@ -1,53 +0,0 @@ -{{ $classes_box := "ba b--dark-gray bg-light-gray br3 flex flex-column flex-wrap items-center justify-center ph3 pv4 mb4 w-100 w-30-l " }} -{{ $gtag := .gtag | default "unknown" }} -{{ $classes_box := "ba b--dark-gray bg-light-gray br3 flex flex-column flex-wrap items-center justify-center ph3 pv4 mb4 w-100 w-30-l " }} -{{ $gtag := .gtag | default "unknown" }} -{{ $isFooter := (eq $gtag "footer") }} -{{ $utmSource := cond $isFooter "hugofooter" "hugohome" }} -{{ with .cx.Site.Data.sponsors }} - -
    -
    -

    Hugo Sponsors

    -
    - {{ range .banners }} -
    - {{ $query_params := .query_params | default "" }} - {{ $url := printf "%s?%s%s" .link $query_params (querify "utm_source" $utmSource "utm_medium" "banner" "utm_campaign" (.utm_campaign | default "hugosponsor")) | safeURL }} - {{ $logo := resources.Get .logo }} - {{ if hugo.IsProduction }} - {{ $gtagID := printf "Sponsor %s %s" .name $gtag | title }} - - {{ $logo.Content | safeHTML }} - - {{ else }} - - {{ $logo.Content | safeHTML }} - - {{ end }} -
    - {{ end }} -
    -
    -
    -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/tweets.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/tweets.html deleted file mode 100644 index 5aebf6737..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/home-page-sections/tweets.html +++ /dev/null @@ -1,25 +0,0 @@ -
    - - {{ $interior_classes := $.Site.Params.flex_box_interior_classes }} - -

    See what others are saying about Hugo…

    - -
    - - {{ range first 4 (sort $.Site.Data.homepagetweets.tweet "date" "desc" ) }} - - {{ end }} -
    -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/after-body-start.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/after-body-start.html deleted file mode 100644 index 426abd018..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/after-body-start.html +++ /dev/null @@ -1 +0,0 @@ -{{/* Deliberately empty */}} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/before-body-end.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/before-body-end.html deleted file mode 100644 index 426abd018..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/hooks/before-body-end.html +++ /dev/null @@ -1 +0,0 @@ -{{/* Deliberately empty */}} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html deleted file mode 100644 index dec9ae48b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/icon-link.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html deleted file mode 100644 index a8fc27e21..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs-mobile.html +++ /dev/null @@ -1,11 +0,0 @@ -{{ $currentPage := . }} -{{ $menu := .Site.Menus.docs.ByWeight }} -
      - {{ range $menu }}{{ $post := printf "%s" .Post }} -
    • - - {{ .Name }} - -
    • - {{end}} -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html deleted file mode 100644 index 61aa11dde..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-docs.html +++ /dev/null @@ -1,23 +0,0 @@ -{{ $currentPage := . }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html deleted file mode 100644 index 6ad98923e..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links-global-mobile.html +++ /dev/null @@ -1,11 +0,0 @@ -{{ $currentPage := . }} -{{ $menu := .Site.Menus.global }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html deleted file mode 100644 index af3790b16..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-links.html +++ /dev/null @@ -1,37 +0,0 @@ -{{ $currentPage := . }} -{{ $.Scratch.Add "listlinkClasses" "f6 link primary-color-dark hover-white db brand-font ma0 w-100 pv3 ph4" }} - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html deleted file mode 100644 index b04866e52..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-mobile.html +++ /dev/null @@ -1,12 +0,0 @@ - -
    - {{ partial "nav-links-docs-mobile.html" . }} -
    - -
    - - - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html deleted file mode 100644 index d8e87eb63..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/nav-top.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ $currentPage := . }} -
    - - - {{ partial "nav-links" .}} -
    - {{ partial "nav-button-open" .}} -
    -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html deleted file mode 100644 index 79b315a44..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/get-featured-image.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ $images := $.Resources.ByType "image" }} -{{ $featured := $images.GetMatch "*feature*" }} -{{ if not $featured }}{{ $featured = $images.GetMatch "{*cover*,*thumbnail*}" }}{{ end }} -{{ if not $featured }} - {{ $featured = resources.Get "/opengraph/gohugoio-card-base-1.png" }} - {{ $size := 80 }} - {{ $title := $.LinkTitle }} - {{ if gt (len $title) 20 }} - {{ $size = 70 }} - {{ end }} - - {{ $text := $title }} - {{ $textOptions := dict - "color" "#FFF" - "size" $size - "lineSpacing" 10 - "x" 65 "y" 80 - "font" (resources.Get "/opengraph/mulish-black.ttf") - }} - - {{ $featured = $featured | images.Filter (images.Text $text $textOptions) }} -{{ end }} - -{{ return $featured }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html deleted file mode 100644 index c8ff64889..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/opengraph.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - -{{- with $.Params.images -}} -{{- range first 6 . }}{{ end -}} -{{- else -}} -{{- $featured := partial "opengraph/get-featured-image.html" . }} -{{- with $featured -}} - -{{- else -}} -{{- with $.Site.Params.images }}{{ end -}} -{{- end -}} -{{- end -}} - -{{- if .IsPage }} -{{- $iso8601 := "2006-01-02T15:04:05-07:00" -}} - -{{ with .PublishDate }}{{ end }} -{{ with .Lastmod }}{{ end }} -{{- end -}} - -{{- with .Params.audio }}{{ end }} -{{- with .Params.locale }}{{ end }} -{{- with .Site.Params.title }}{{ end }} -{{- with .Params.videos }}{{- range . }} - -{{ end }}{{ end }} - -{{- /* If it is part of a series, link to related articles */}} -{{- $permalink := .Permalink }} -{{- $siteSeries := .Site.Taxonomies.series }} -{{ with .Params.series }}{{- range $name := . }} - {{- $series := index $siteSeries ($name | urlize) }} - {{- range $page := first 6 $series.Pages }} - {{- if ne $page.Permalink $permalink }}{{ end }} - {{- end }} -{{ end }}{{ end }} - -{{- /* Facebook Page Admin ID for Domain Insights */}} -{{- with .Site.Social.facebook_admin }}{{ end }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html deleted file mode 100644 index 9d25d0315..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/opengraph/twitter_cards.html +++ /dev/null @@ -1,22 +0,0 @@ -{{- with $.Params.images -}} - - -{{ else -}} -{{- $featured := partial "opengraph/get-featured-image.html" . }} -{{- with $featured -}} - - -{{- else -}} -{{- with $.Site.Params.images -}} - - -{{ else -}} - -{{- end -}} -{{- end -}} -{{- end }} - - -{{ with .Site.Social.twitter -}} - -{{ end -}} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html deleted file mode 100644 index edf84669e..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-edit.html +++ /dev/null @@ -1,3 +0,0 @@ -Improve this page diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html deleted file mode 100644 index dcc96242f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/page-header.html +++ /dev/null @@ -1,20 +0,0 @@ -{{ $currentPage := . }} -{{ $currentURL := .RelPermalink }} -
    -
      - -
    • - - News: - -
    • - {{ range $name, $taxonomy := .Site.Taxonomies.categories }} - {{ $link := $name | printf "%s%s" "/categories/" | printf "%s/" }} -
    • - - {{ $name | humanize }} - -
    • - {{ end }} -
    -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html deleted file mode 100644 index dd048223e..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/pagelayout.html +++ /dev/null @@ -1,34 +0,0 @@ -{{ $section_to_display := .section_to_display }} -
    - -
    -
    - {{ partial "nav-links-docs.html" .context }} -
    - -
    - - - - -
    - {{ $interior_classes := .context.Site.Params.flex_box_interior_classes }} -
    - {{ range $section_to_display }} - {{ partial "boxes-section-summaries" (dict "context" . "classes" $interior_classes "fullcontent" true) }} - {{ end }} -
    -
    - -
    - -
    - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html deleted file mode 100644 index 71a14c0ef..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section-with-title.html +++ /dev/null @@ -1,14 +0,0 @@ -{{ if or .PrevInSection .NextInSection }} -{{/* this div holds these a tags as a unit for flex-box display */}} - -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html deleted file mode 100644 index af9f4aac1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links-in-section.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ if or .PrevInSection .NextInSection }} -{{/* this div holds these a tags as a unit for flex-box display */}} - -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html deleted file mode 100644 index cd43dd840..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/previous-next-links.html +++ /dev/null @@ -1,25 +0,0 @@ -{{if .Prev }} - - {{ partial "svg/ic_chevron_left_black_24px.svg" (dict "size" "30px") }} {{ .Prev.Title }} - -{{end}} - -{{if .Next }} - - {{ .Next.Title }} {{ partial "svg/ic_chevron_right_black_24px.svg" (dict "size" "30px") }} - -{{end}} - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html deleted file mode 100644 index fb11699af..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/related.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ $related := .Site.RegularPages.Related . | first 5 }} -{{ with $related }} -

    See Also

    - -{{ end }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html deleted file mode 100644 index 09c013361..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-footer.html +++ /dev/null @@ -1,48 +0,0 @@ -
    -
    -
    - - - -
    - Hugo Logo -
    - - - - - - {{ with getenv "REPOSITORY_URL" -}} -

    Netlify badge

    - {{- end }} - -
    - -
      - {{ partial "home-page-sections/sponsors.html" (dict "cx" . "gtag" "footer" "classes_section" "pb3 w-100" "classes_copy" "f7 w-90-ns") }} -
    - -
    - -
      -

    The Hugo logos are copyright © Steve Francia 2013–{{ now.Year }}.

    -

    The Hugo Gopher is based on an original work by Renée French.

    -
    - - - -
    - {{- partial "nav-mobile.html" . -}} -
    - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html deleted file mode 100644 index 54472ba16..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-manifest.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html deleted file mode 100644 index 749c699e6..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-nav.html +++ /dev/null @@ -1,38 +0,0 @@ -{{ $currentPage := . }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html deleted file mode 100644 index 7dec9de18..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-scripts.html +++ /dev/null @@ -1,17 +0,0 @@ - -{{ $scripts := resources.Get "output/js/app.js" }} -{{ $isDev := eq hugo.Environment "development" }} -{{ if not $isDev }} -{{ $scripts = $scripts | fingerprint }} -{{ end }} -{{ with $scripts }} - {{ if $isDev }} - - {{ else }} - - {{ end }} - {{ $.Scratch.Set "scripts" . }} -{{end}} - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html deleted file mode 100644 index 8c97ac454..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/site-search.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html deleted file mode 100644 index 7b517dbb4..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/social-follow.html +++ /dev/null @@ -1,7 +0,0 @@ - -{{ with .Site.Social.twitter }} - -{{ end }} -Star diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html deleted file mode 100644 index 0f140cf70..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/summary.html +++ /dev/null @@ -1,13 +0,0 @@ -
    -
    - {{ humanize .Section }} -

    - - {{ .Title }} - -

    - -
    -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg deleted file mode 100644 index da9438414..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/Twitter_Logo_Blue.svg +++ /dev/null @@ -1 +0,0 @@ -Twitter_Logo_Blue diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg deleted file mode 100644 index 6f3c20f76..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/apple.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg deleted file mode 100644 index e1b170359..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clipboard.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg deleted file mode 100644 index e1b170359..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/clippy.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg deleted file mode 100644 index 2ea15de87..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/cloud.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg deleted file mode 100644 index bc696b90b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/content.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg deleted file mode 100644 index 9f9d71769..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/design.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg deleted file mode 100644 index 6e6af44a2..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/facebook.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg deleted file mode 100644 index ed2c929b4..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/focus.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg deleted file mode 100644 index 842be09a1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/freebsd.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg deleted file mode 100644 index 717a35686..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/functions.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg deleted file mode 100644 index 29bc57ad3..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-corner.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg deleted file mode 100644 index dabc741e0..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/github-squared.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg deleted file mode 100644 index 9c2de7da2..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gitter.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg deleted file mode 100644 index 9ab114aa3..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gme.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html deleted file mode 100644 index 1a6b82159..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/godoc-icon.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg deleted file mode 100644 index 961221f18..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-2.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg deleted file mode 100644 index 0f8fbe0d9..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-front.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg deleted file mode 100644 index 36d9f1c41..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-homepage.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg deleted file mode 100644 index 05cfb84d1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-side_path.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg deleted file mode 100644 index bc1e5010c..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher-small.svg +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg deleted file mode 100644 index 7f6ec255c..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/gopher.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg deleted file mode 100644 index ea72a6f51..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo-h-only.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg deleted file mode 100644 index 58d025596..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/hugo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg deleted file mode 100644 index 3ba28c3f5..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_down.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg deleted file mode 100644 index 8ec2eb766..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_arrow_drop_up.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg deleted file mode 100644 index da37757cf..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_left_black_24px.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg deleted file mode 100644 index 47689a91e..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/ic_chevron_right_black_24px.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg deleted file mode 100644 index 5c2ccc2f4..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/idea.svg +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg deleted file mode 100644 index ae915113b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/instagram.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg deleted file mode 100644 index b0e2f5b0d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/javascript.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg deleted file mode 100644 index d2ba6d0fc..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/json.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg deleted file mode 100644 index ba9400b7f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-ext.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg deleted file mode 100644 index f5de52d02..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/link-permalink.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg deleted file mode 100644 index f1a794565..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/md.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg deleted file mode 100644 index d0d9ae938..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/mdsolid.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - Svg Vector Icons : http://www.onlinewebfonts.com/icon - - \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg deleted file mode 100644 index 83b706383..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/newlogo.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg deleted file mode 100644 index da3d9cfcf..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/sass.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg deleted file mode 100644 index 181789b54..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/search.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg deleted file mode 100644 index 247ca9062..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/twitter.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg deleted file mode 100644 index 2bdcf5f94..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/website.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg deleted file mode 100644 index fe3bf0296..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/windows.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg deleted file mode 100644 index 59eeb71c2..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/svg/yaml.svg +++ /dev/null @@ -1 +0,0 @@ -icon \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html deleted file mode 100644 index 59e3e51a0..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/tags.html +++ /dev/null @@ -1,37 +0,0 @@ -{{ $currentPageUrl := .RelPermalink }} -{{ if and .Params.tags .Site.Taxonomies.tags }} - {{ $name := index .Params.tags 0 }} - {{ $name := $name | urlize }} - {{ $tags := index .Site.Taxonomies.tags $name }} - -
    - -
    -{{end}} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/toc.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/toc.html deleted file mode 100644 index 583feec4f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/partials/toc.html +++ /dev/null @@ -1,13 +0,0 @@ - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt deleted file mode 100644 index 25b9e9a0d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/robots.txt +++ /dev/null @@ -1,8 +0,0 @@ -User-agent: * -# robotstxt.org - if ENV production variable is false robots will be disallowed. -{{ if eq (getenv "HUGO_ENV") "production" }} - Disallow: admin/ - Disallow: -{{ else }} - Disallow: / -{{ end }} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html deleted file mode 100644 index 2755b1e2d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/articlelist.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - {{ range $ind, $art := $.Site.Data.articles.article }} - - - - - - {{ end }} - -
    Title - Author - Date -
    {{$art.title | markdownify }}{{ $art.author | markdownify }}{{ $art.date }}
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html deleted file mode 100644 index c695a7aae..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code-toggle.html +++ /dev/null @@ -1,24 +0,0 @@ -{{ $langs := (slice "yaml" "toml" "json") }} -
    -
    - {{- with .Get "file" -}} -
    {{ . }}.
    - {{- end -}} - {{ range $langs }} -   - {{ end }} -
    -
    - {{ range $langs }} -
    - {{ highlight ($.Inner | transform.Remarshal . | safeHTML) . ""}} -
    - {{ if ne ($.Get "copy") "false" }} - - {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}} - {{end}} - {{ end }} -
    - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html deleted file mode 100644 index 6df49956a..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/code.html +++ /dev/null @@ -1,15 +0,0 @@ -
    - {{- with .Get "file" -}} -
    {{.}}
    - {{- end -}} - - {{ if ne (.Get "copy") "false" }} - - {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}} - {{end}} -
    - {{- .Inner -}} -
    - -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html deleted file mode 100644 index 7ddda86d0..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/datatable.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - {{ range $ind, $art := $.Site.Data.articles.article }} - - - - - - {{ end }} - -
    TitleAuthorDate
    {{$art.title | markdownify }}{{ $art.author | markdownify }}{{ $art.date }}
    \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/directoryindex.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/directoryindex.html deleted file mode 100644 index 37e7d3ad1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/directoryindex.html +++ /dev/null @@ -1,13 +0,0 @@ -{{- $pathURL := .Get "pathURL" -}} -{{- $path := .Get "path" -}} -{{- $files := readDir $path -}} - - - -{{- range $files }} - - - - -{{- end }} -
    Size in bytesName
    {{ .Size }} {{ .Name }}
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/docfile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/docfile.html deleted file mode 100644 index 2f982aae8..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/docfile.html +++ /dev/null @@ -1,11 +0,0 @@ -{{ $file := .Get 0}} -{{ $filepath := $file }} -{{ $syntax := index (split $file ".") 1 }} -{{ $syntaxoverride := eq (len .Params) 2 }} -
    -
    {{$filepath}}
    - -
    {{- readFile $file -}}
    -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfile.html deleted file mode 100644 index 226782957..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfile.html +++ /dev/null @@ -1,12 +0,0 @@ -{{ $file := .Get 0}} -{{ $filepath := replace $file "static/" ""}} -{{ $syntax := index (split $file ".") 1 }} -{{ $syntaxoverride := eq (len .Params) 2 }} -
    -
    {{$filepath}}
    - -
    {{- readFile $file -}}
    - Source -
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfm.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfm.html deleted file mode 100644 index c0429bbe1..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/exfm.html +++ /dev/null @@ -1,13 +0,0 @@ - -{{ $file := .Get 0}} -{{ $filepath := replace $file "static/" ""}} -{{ $syntax := index (split $file ".") 1 }} -{{ $syntaxoverride := eq (len .Params) 2 }} -
    -
    {{$filepath}}
    - -
    {{- readFile $file -}}
    - Source -
    \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gh.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gh.html deleted file mode 100644 index e027dc0f0..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/gh.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ range .Params }} - {{ if eq (substr . 0 1) "@" }} - {{ . }} - {{ else if eq (substr . 0 2) "0x" }} - {{ substr . 2 6 }} - {{ else }} - #{{ . }} - {{ end }} -{{ end }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/ghrepo.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/ghrepo.html deleted file mode 100644 index e9df40d6a..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/ghrepo.html +++ /dev/null @@ -1 +0,0 @@ -GitHub repository \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/nohighlight.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/nohighlight.html deleted file mode 100644 index 0f254b4ca..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/nohighlight.html +++ /dev/null @@ -1 +0,0 @@ -
    {{ .Inner }}
    diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html deleted file mode 100644 index 24d2cd0b2..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/note.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ $_hugo_config := `{ "version": 1 }` }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/output.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/output.html deleted file mode 100644 index df1a8ae89..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/output.html +++ /dev/null @@ -1,11 +0,0 @@ -{{$file := .Get "file"}} -{{$icon := index (split $file ".") 1 }} -
    -
    {{$file}}
    - -
    - {{- .Inner -}} -
    -
    \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html deleted file mode 100644 index f777abe26..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/readfile.html +++ /dev/null @@ -1,6 +0,0 @@ -{{$file := .Get "file"}} -{{- if eq (.Get "markdown") "true" -}} -{{- $file | readFile | markdownify -}} -{{- else -}} -{{ $file | readFile | safeHTML }} -{{- end -}} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/tip.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/tip.html deleted file mode 100644 index 139e3376b..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/tip.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ $_hugo_config := `{ "version": 1 }` }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/warning.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/warning.html deleted file mode 100644 index c9147be64..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/warning.html +++ /dev/null @@ -1,9 +0,0 @@ -{{ $_hugo_config := `{ "version": 1 }` }} - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/yt.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/yt.html deleted file mode 100644 index 6915cec5f..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/shortcodes/yt.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - - {{if (.Get "thumbnail")}} -
    - {{else}} -
    - {{end}} -
    -{{ if (.Get "description") }} -
    {{ .Get "description" | markdownify }}
    -{{ end }} \ No newline at end of file diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html deleted file mode 100644 index bff52ad8d..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/list.html +++ /dev/null @@ -1,46 +0,0 @@ -{{ define "main" }} -
    -
    -

    - {{ .Title }} -

    -
    - {{ .Content }} -
    -
    -
    - {{ range (.Paginate (.Pages | shuffle ) 20).Pages }} - {{template "showcase_items" .}} - {{ end }} -
    - -
    The Showcase articles are copyrighted by their respective content authors. Any open source license will be attached.
    -
    -{{ end }} - - -{{define "showcase_items"}} - -
    - {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }} - {{ with $img }} - {{ $big := .Fill "1024x512 top" }} - {{ $small := $big.Resize "512x" }} - - {{end}} -
    {{/* the margin aligns to the bottom */}} -

    - {{- .Title -}} -

    -
    -
    -
    - - -{{end}} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html b/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html deleted file mode 100644 index 5ae1e07a7..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/layouts/showcase/single.html +++ /dev/null @@ -1,106 +0,0 @@ -{{ define "title" }} -Showcase: {{ .Title }} -{{ end }} - -{{ define "main" }} -
    - - -
    - -
    - {{template "sc-details" .}} -
    - -
    - {{template "sc-main-column" .}} -
    - - - -
    - -
    {{/* bottom row */}} - Last Update: {{ .Lastmod.Format "January 2, 2006" }}
    - {{ partial "page-edit.html" . }} -
    -
    The Showcase articles are copyright the content authors. Any open source license will be attached.
    -
    -{{ end }} - - - -{{define "sc-main-column"}} - {{ $img := (.Resources.ByType "image").GetMatch "*featured*" }} - {{ with $img }} - {{ $big := .Fill "1024x512 top" }} - {{ $small := $big.Resize "512x" }} - {{ $img.Title }} - {{ end }} - - -{{end}} - -{{define "sc-details"}} - -{{end}} - -{{define "sc-navigation"}} - {{$section := where .Site.RegularPages "Section" .Section}} - {{$number_of_entries := $section | len}} - -{{end}} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json b/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json deleted file mode 100644 index 14d128910..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "gohugo-default-styles", - "version": "1.1.0", - "description": "Default Theme for Hugo Sites", - "main": "index.js", - "repository": "", - "author": "budparr", - "license": "MIT", - "scripts": { - "build": "NODE_ENV=production webpack", - "build-dev": "NODE_ENV=development webpack --progress --watch", - "start": "npm run build-dev" - }, - "devDependencies": { - "clean-webpack-plugin": "^1.0.0", - "clipboard": "^2.0.4", - "css-loader": "^1.0.1", - "docsearch.js": "^2.6.1", - "file-loader": "^2.0.0", - "glob-all": "^3.1.0", - "lazysizes": "^5.3.2", - "mini-css-extract-plugin": "^0.4.4", - "postcss": "^7.0.5", - "postcss-cssnext": "^3.1.0", - "postcss-import": "^12.0.1", - "postcss-loader": "^3.0.0", - "purgecss-webpack-plugin": "^1.3.1", - "scrolldir": "^1.4.0", - "tachyons": "^4.7.0", - "typeface-muli": "0.0.54", - "webpack": "^4.25.1", - "webpack-command": "^0.4.2" - } -} diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png deleted file mode 100644 index ecf1fc020..000000000 Binary files a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/apple-touch-icon.png and /dev/null differ diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml deleted file mode 100644 index 62400c5f2..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/browserconfig.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - #2d89ef - - - diff --git a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js b/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js deleted file mode 100644 index 6391e71e9..000000000 --- a/docs/_vendor/github.com/gohugoio/gohugoioTheme/static/dist/app.bundle.js +++ /dev/null @@ -1,22 +0,0 @@ -!function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=11)}([function(t,e,n){"use strict";var r=function(t){var e=document.createElement("a");return e.className="header-link",e.href="#"+t,e.innerHTML=' ',e},i=function(t,e){for(var n=e.getElementsByTagName("h"+t),i=0;i0&&p.parentNode.classList.add("expand")}}catch(t){a=!0,u=t}finally{try{!s&&l.return&&l.return()}finally{if(a)throw u}}}},function(t,e,n){"use strict";n(13)({apiKey:"167e7998590aebda7f9fedcf86bc4a55",indexName:"hugodocs",inputSelector:"#search-input",debug:!0})},function(t,e,n){"use strict";n(14),n(15)},function(t,e,n){"use strict";function r(){for(var t=this.dataset.target.split(" "),e=document.querySelector(".mobilemenu:not(.dn)"),n=document.querySelector(".desktopmenu:not(.dn)"),r=document.querySelector(".desktopmenu:not(.dn)"),i=0;i=0?function(){var t=window.pageYOffset;(t>=i-s||window.innerHeight+t>=document.body.offsetHeight)&&clearInterval(u)}:function(){window.pageYOffset<=(i||0)&&clearInterval(u)};var u=setInterval(a,16)},e=document.querySelectorAll("#TableOfContents ul li a");[].forEach.call(e,function(e){e.addEventListener("click",function(n){n.preventDefault();var r=e.getAttribute("href"),i=document.querySelector(r),o=e.getAttribute("data-speed");i&&t(i,o||500)},!1)})}}()},function(t,e,n){"use strict";function r(t){if(t.target){t.preventDefault();var e=t.currentTarget,n=e.getAttribute("data-toggle-tab")}else var n=t;window.localStorage&&window.localStorage.setItem("configLangPref",n);for(var r=document.querySelectorAll("[data-toggle-tab='"+n+"']"),i=document.querySelectorAll("[data-pane='"+n+"']"),a=0;a0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,r.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,r.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":i(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=s})},{select:5}],8:[function(e,n,r){!function(i,o){if("function"==typeof t&&t.amd)t(["module","./clipboard-action","tiny-emitter","good-listener"],o);else if(void 0!==r)o(n,e("./clipboard-action"),e("tiny-emitter"),e("good-listener"));else{var s={exports:{}};o(s,i.clipboardAction,i.tinyEmitter,i.goodListener),i.clipboard=s.exports}}(this,function(t,e,n,r){"use strict";function i(t){return t&&t.__esModule?t:{default:t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function s(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function u(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}var c=i(e),l=i(n),h=i(r),f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},p=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===f(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,h.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new c.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(l.default);t.exports=d})},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)})},function(t,e,n){/*! docsearch 2.4.1 | © Algolia | github.com/algolia/docsearch */ -!function(e,n){t.exports=n()}(0,function(){return function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=46)}([function(t,e,n){"use strict";function r(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}var i=n(1);t.exports={isArray:null,isFunction:null,isObject:null,bind:null,each:null,map:null,mixin:null,isMsie:function(){return!!/(msie|trident)/i.test(navigator.userAgent)&&navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]},escapeRegExChars:function(t){return t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isNumber:function(t){return"number"==typeof t},toStr:function(t){return void 0===t||null===t?"":t+""},cloneDeep:function(t){var e=this.mixin({},t),n=this;return this.each(e,function(t,r){t&&(n.isArray(t)?e[r]=[].concat(t):n.isObject(t)&&(e[r]=n.cloneDeep(t)))}),e},error:function(t){throw new Error(t)},every:function(t,e){var n=!0;return t?(this.each(t,function(r,i){if(!(n=e.call(null,r,i,t)))return!1}),!!n):n},any:function(t,e){var n=!1;return t?(this.each(t,function(r,i){if(e.call(null,r,i,t))return n=!0,!1}),n):n},getUniqueId:function(){var t=0;return function(){return t++}}(),templatify:function(t){if(this.isFunction(t))return t;var e=i.element(t);return"SCRIPT"===e.prop("tagName")?function(){return e.text()}:function(){return String(t)}},defer:function(t){setTimeout(t,0)},noop:function(){},formatPrefix:function(t,e){return e?"":t+"-"},className:function(t,e,n){return(n?"":".")+t+e},escapeHighlightedString:function(t,e,n){e=e||"";var i=document.createElement("div");i.appendChild(document.createTextNode(e)),n=n||"";var o=document.createElement("div");o.appendChild(document.createTextNode(n));var s=document.createElement("div");return s.appendChild(document.createTextNode(t)),s.innerHTML.replace(RegExp(r(i.innerHTML),"g"),e).replace(RegExp(r(o.innerHTML),"g"),n)}}},function(t,e,n){"use strict";t.exports={element:null}},function(t,e){var n=Object.prototype.hasOwnProperty,r=Object.prototype.toString;t.exports=function(t,e,i){if("[object Function]"!==r.call(e))throw new TypeError("iterator must be a function");var o=t.length;if(o===+o)for(var s=0;s was loaded but did not call our provided callback"),JSONPScriptError:i("JSONPScriptError"," + +``` + +The delimiters above must match the delimiters in your site configuration. + +### Step 3 + +Conditionally call the partial template from the base template. + +```go-html-template {file="layouts/_default/baseof.html"} + + ... + {{ if .Param "math" }} + {{ partialCached "math.html" . }} + {{ end }} + ... + +``` + +The example above loads the partial template if you have set the `math` parameter in front matter to `true`. If you have not set the `math` parameter in front matter, the conditional statement falls back to the `math` parameter in your site configuration. + +### Step 4 + +Include mathematical equations and expressions in Markdown using LaTeX markup. + +```text {file="content/math-examples.md" copy=true} +This is an inline \(a^*=x-b^*\) equation. + +These are block equations: + +\[a^*=x-b^*\] + +\[ a^*=x-b^* \] + +\[ +a^*=x-b^* +\] + +These are also block equations: + +$$a^*=x-b^*$$ + +$$ a^*=x-b^* $$ + +$$ +a^*=x-b^* +$$ +``` + +If you set the `math` parameter to `false` in your site configuration, you must set the `math` parameter to `true` in front matter. For example: + +{{< code-toggle file=content/math-examples.md fm=true >}} +title = 'Math examples' +date = 2024-01-24T18:09:49-08:00 +[params] +math = true +{{< /code-toggle >}} + +## Inline delimiters + +The configuration, JavaScript, and examples above use the `\(...\)` delimiter pair for inline equations. The `$...$` delimiter pair is a common alternative, but using it may result in unintended formatting if you use the `$` symbol outside of math contexts. + +If you add the `$...$` delimiter pair to your configuration and JavaScript, you must double-escape the `$` when outside of math contexts, regardless of whether mathematical rendering is enabled on the page. For example: + +```text +A \\$5 bill _saved_ is a \\$5 bill _earned_. +``` + +> [!note] +> If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437). + +## Engines + +MathJax and KaTeX are open-source JavaScript display engines. Both engines are fast, but at the time of this writing MathJax v3.2.2 is slightly faster than KaTeX v0.16.11. + +> [!note] +> If you use the `$...$` delimiter pair for inline equations, and occasionally use the `$` symbol outside of math contexts, you must use MathJax instead of KaTeX to avoid unintended formatting caused by [this KaTeX limitation](https://github.com/KaTeX/KaTeX/issues/437). +> +>See the [inline delimiters](#inline-delimiters) section for details. + +To use KaTeX instead of MathJax, replace the partial template from [Step 2] with this: + +```go-html-template {file="layouts/partials/math.html" copy=true} + + + + +``` + +The delimiters above must match the delimiters in your site configuration. + +## Chemistry + +Both MathJax and KaTeX provide support for chemical equations. For example: + +```text +$$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$ +``` + +$$C_p[\ce{H2O(l)}] = \pu{75.3 J // mol K}$$ + +As shown in [Step 2] above, MathJax supports chemical equations without additional configuration. To add chemistry support to KaTeX, enable the mhchem extension as described in the KaTeX [documentation](https://katex.org/docs/libs). + +[`transform.ToMath`]: /functions/transform/tomath/ +[KaTeX]: https://katex.org/ +[LaTeX]: https://www.latex-project.org/ +[MathJax]: https://www.mathjax.org/ +[passthrough extension]: /configuration/markup/#passthrough +[Step 2]: #step-2 +[Step 3]: #step-3 diff --git a/docs/content/en/content-management/menus.md b/docs/content/en/content-management/menus.md index 07bf41669..6d01173dc 100644 --- a/docs/content/en/content-management/menus.md +++ b/docs/content/en/content-management/menus.md @@ -1,14 +1,8 @@ --- title: Menus -description: Create menus by defining entries, localizing each entry, and rendering the resulting data structure. -categories: [content management] -keywords: [menus] -menu: - docs: - parent: content-management - weight: 190 -toc: true -weight: 190 +description: Create menus by defining entries, localizing each entry, and rendering the resulting data structure. +categories: [] +keywords: [] aliases: [/extras/menus/] --- @@ -17,8 +11,8 @@ aliases: [/extras/menus/] To create a menu for your site: 1. Define the menu entries -2. [Localize] each entry -3. Render the menu with a [template] +1. [Localize](multilingual/#menus) each entry +1. Render the menu with a [template] Create multiple menus, either flat or nested. For example, create a main menu for the header, and a separate menu for the footer. @@ -28,15 +22,14 @@ There are three ways to define menu entries: 1. In front matter 1. In site configuration -{{% note %}} -Although you can use these methods in combination when defining a menu, the menu will be easier to conceptualize and maintain if you use one method throughout the site. -{{% /note %}} +> [!note] +> Although you can use these methods in combination when defining a menu, the menu will be easier to conceptualize and maintain if you use one method throughout the site. ## Define automatically -To automatically define menu entries for each top-level section of your site, enable the section pages menu in your site configuration. +To automatically define a menu entry for each top-level [section](g) of your site, enable the section pages menu in your site configuration. -{{< code-toggle file="hugo" copy=false >}} +{{< code-toggle file=hugo >}} sectionPagesMenu = "main" {{< /code-toggle >}} @@ -46,170 +39,50 @@ This creates a menu structure that you can access with `site.Menus.main` in your To add a page to the "main" menu: -{{< code-toggle file="content/about.md" copy=false fm=true >}} +{{< code-toggle file=content/about.md fm=true >}} title = 'About' -menu = 'main' +menus = 'main' {{< /code-toggle >}} Access the entry with `site.Menus.main` in your templates. See [menu templates] for details. To add a page to the "main" and "footer" menus: -{{< code-toggle file="content/contact.md" copy=false fm=true >}} +{{< code-toggle file=content/contact.md fm=true >}} title = 'Contact' -menu = ['main','footer'] +menus = ['main','footer'] {{< /code-toggle >}} Access the entry with `site.Menus.main` and `site.Menus.footer` in your templates. See [menu templates] for details. -### Properties {#properties-front-matter} +> [!note] +> The configuration key in the examples above is `menus`. The `menu` (singular) configuration key is an alias for `menus`. + +### Properties Use these properties when defining menu entries in front matter: -identifier -: (`string`) Required when two or more menu entries have the same `name`, or when localizing the `name` using translation tables. Must start with a letter, followed by letters, digits, or underscores. +{{% include "/_common/menu-entry-properties.md" %}} -name -: (`string`) The text to display when rendering the menu entry. - -params -: (`map`) User-defined properties for the menu entry. - -parent -: (`string`) The `identifier` of the parent menu entry. If `identifier` is not defined, use `name`. Required for child entries in a nested menu. - -post -: (`string`) The HTML to append when rendering the menu entry. - -pre -: (`string`) The HTML to prepend when rendering the menu entry. - -title -: (`string`) The HTML `title` attribute of the rendered menu entry. - -weight -: (`int`) A non-zero integer indicating the entry's position relative the root of the menu, or to its parent for a child entry. Lighter entries float to the top, while heavier entries sink to the bottom. - -### Example {#example-front-matter} +### Example This front matter menu entry demonstrates some of the available properties: -{{< code-toggle file="content/products/software.md" copy=false fm=true >}} +{{< code-toggle file=content/products/software.md fm=true >}} title = 'Software' -[menu.main] +[menus.main] parent = 'Products' weight = 20 pre = '' -[menu.main.params] +[menus.main.params] class = 'center' {{< /code-toggle >}} Access the entry with `site.Menus.main` in your templates. See [menu templates] for details. - ## Define in site configuration -To define entries for the "main" menu: - -{{< code-toggle file="hugo" copy=false >}} -[[menu.main]] -name = 'Home' -pageRef = '/' -weight = 10 - -[[menu.main]] -name = 'Products' -pageRef = '/products' -weight = 20 - -[[menu.main]] -name = 'Services' -pageRef = '/services' -weight = 30 -{{< /code-toggle >}} - -This creates a menu structure that you can access with `site.Menus.main` in your templates. See [menu templates] for details. - -To define entries for the "footer" menu: - -{{< code-toggle file="hugo" copy=false >}} -[[menu.footer]] -name = 'Terms' -pageRef = '/terms' -weight = 10 - -[[menu.footer]] -name = 'Privacy' -pageRef = '/privacy' -weight = 20 -{{< /code-toggle >}} - -This creates a menu structure that you can access with `site.Menus.footer` in your templates. See [menu templates] for details. - -### Properties {#properties-site-configuration} - -{{% note %}} -The [properties available to entries defined in front matter] are also available to entries defined in site configuration. - -[properties available to entries defined in front matter]: /content-management/menus/#properties-front-matter -{{% /note %}} - -Each menu entry defined in site configuration requires two or more properties: - -- Specify `name` and `pageRef` for internal links -- Specify `name` and `url` for external links - -pageRef -: (`string`) The file path of the target page, relative to the `content` directory. Omit language code and file extension. Required for *internal* links. - -Kind|pageRef -:--|:-- -home|`/` -page|`/books/book-1` -section|`/books` -taxonomy|`/tags` -term|`/tags/foo` - -url -: (`string`) Required for *external* links. - -### Example {#example-site-configuration} - -This nested menu demonstrates some of the available properties: - -{{< code-toggle file="hugo" copy=false >}} -[[menu.main]] -name = 'Products' -pageRef = '/products' -weight = 10 - -[[menu.main]] -name = 'Hardware' -pageRef = '/products/hardware' -parent = 'Products' -weight = 1 - -[[menu.main]] -name = 'Software' -pageRef = '/products/software' -parent = 'Products' -weight = 2 - -[[menu.main]] -name = 'Services' -pageRef = '/services' -weight = 20 - -[[menu.main]] -name = 'Hugo' -pre = '' -url = 'https://gohugo.io/' -weight = 30 -[menu.main.params] -rel = 'external' -{{< /code-toggle >}} - -This creates a menu structure that you can access with `site.Menus.main` in your templates. See [menu templates] for details. +See [configure menus](/configuration/menus/). ## Localize @@ -219,7 +92,6 @@ Hugo provides two methods to localize your menu entries. See [multilingual]. See [menu templates]. -[localize]: /content-management/multilingual/#menus -[menu templates]: /templates/menu-templates/ +[menu templates]: /templates/menu/ [multilingual]: /content-management/multilingual/#menus -[template]: /templates/menu-templates/ +[template]: /templates/menu/ diff --git a/docs/content/en/content-management/multilingual.md b/docs/content/en/content-management/multilingual.md index 71adc214d..d419f4381 100644 --- a/docs/content/en/content-management/multilingual.md +++ b/docs/content/en/content-management/multilingual.md @@ -1,200 +1,42 @@ --- -title: Multilingual Mode +title: Multilingual mode linkTitle: Multilingual -description: Hugo supports the creation of websites with multiple languages side by side. -categories: [content management] -keywords: [multilingual,i18n, internationalization] -menu: - docs: - parent: content-management - weight: 230 -toc: true -weight: 230 +description: Localize your project for each language and region, including translations, images, dates, currencies, numbers, percentages, and collation sequence. Hugo's multilingual framework supports single-host and multihost configurations. +categories: [] +keywords: [] aliases: [/content/multilingual/,/tutorials/create-a-multilingual-site/] --- -You should define the available languages in a `languages` section in your site configuration. +## Configuration -Also See [Hugo Multilingual Part 1: Content translation]. +See [configure languages](/configuration/languages/). -## Configure Languages - -The following is an example of a site configuration for a multilingual Hugo project: - -{{< code-toggle file="hugo" >}} -defaultContentLanguage = "en" -copyright = "Everything is mine" - -[params] -[params.navigation] -help = "Help" - -[languages] -[languages.en] -title = "My blog" -weight = 1 -[languages.en.params] -linkedin = "https://linkedin.com/whoever" - -[languages.fr] -title = "Mon blogue" -weight = 2 -[languages.fr.params] -linkedin = "https://linkedin.com/fr/whoever" -[languages.fr.params.navigation] -help = "Aide" - -[languages.ar] -title = "مدونتي" -weight = 2 -languagedirection = "rtl" - -[languages.pt-pt] -title = "O meu blog" -weight = 3 -{{< /code-toggle >}} - -Anything not defined in a `languages` block will fall back to the global value for that key (e.g., `copyright` for the English `en` language). This also works for `params`, as demonstrated with `help` above: You will get the value `Aide` in French and `Help` in all the languages without this parameter set. - -With the configuration above, all content, sitemap, RSS feeds, pagination, -and taxonomy pages will be rendered below `/` in English (your default content language) and then below `/fr` in French. - -When working with front matter `Params` in [single page templates], omit the `params` in the key for the translation. - -`defaultContentLanguage` sets the project's default language. If not set, the default language will be `en`. - -If the default language needs to be rendered below its own language code (`/en`) like the others, set `defaultContentLanguageInSubdir: true`. - -Only the obvious non-global options can be overridden per language. Examples of global options are `baseURL`, `buildDrafts`, etc. - -**Please note:** use lowercase language codes, even when using regional languages (ie. use pt-pt instead of pt-PT). Currently Hugo language internals lowercase language codes, which can cause conflicts with settings like `defaultContentLanguage` which are not lowercased. Please track the evolution of this issue in [Hugo repository issue tracker](https://github.com/gohugoio/hugo/issues/7344) - -### Changes in Hugo 0.112.0 - -{{< new-in "0.112.0" >}} - -In Hugo `v0.112.0` we consolidated all configuration options, and improved how the languages and their parameters are merged with the main configuration. But while testing this on Hugo sites out there, we received some error reports and reverted some of the changes in favor of deprecation warnings: - -1. `site.Language.Params` is deprecated. Use `site.Params` directly. -1. Adding custom params to the top level language config is deprecated, add all of these below `[params]`, see `color` in the example below. - -```toml -title = "My blog" -languageCode = "en-us" - -[languages] -[languages.sv] -title = "Min blogg" -languageCode = "sv" -[languages.en.params] -color = "blue" -``` - -In the example above, all settings except `color` below `params` map to predefined configuration options in Hugo for the site and its language, and should be accessed via the documented accessors: - -``` -{{ site.Title }} -{{ site.LanguageCode }} -{{ site.Params.color }} -``` - -### Disable a Language - -You can disable one or more languages. This can be useful when working on a new translation. - -{{< code-toggle file="hugo" >}} -disableLanguages = ["fr", "ja"] -{{< /code-toggle >}} - -Note that you cannot disable the default content language. - -We kept this as a standalone setting to make it easier to set via [OS environment]: - -```bash -HUGO_DISABLELANGUAGES="fr ja" hugo -``` - -If you have already a list of disabled languages in `hugo.toml`, you can enable them in development like this: - -```bash -HUGO_DISABLELANGUAGES=" " hugo server -``` - -### Configure Multilingual Multihost - -From **Hugo 0.31** we support multiple languages in a multihost configuration. See [this issue](https://github.com/gohugoio/hugo/issues/4027) for details. - -This means that you can now configure a `baseURL` per `language`: - -{{% note %}} -If a `baseURL` is set on the `language` level, then all languages must have one and they must all be different. -{{% /note %}} - -Example: - -{{< code-toggle file="hugo" >}} -[languages] -[languages.fr] -baseURL = "https://example.fr" -languageName = "Français" -weight = 1 -title = "En Français" - -[languages.en] -baseURL = "https://example.com" -languageName = "English" -weight = 2 -title = "In English" -{{}} - -With the above, the two sites will be generated into `public` with their own root: - -```text -public -├── en -└── fr -``` - -**All URLs (i.e `.Permalink` etc.) will be generated from that root. So the English home page above will have its `.Permalink` set to `https://example.com/`.** - -When you run `hugo server` we will start multiple HTTP servers. You will typically see something like this in the console: - -```text -Web Server is available at 127.0.0.1:1313 (bind address 127.0.0.1) -Web Server is available at 127.0.0.1:1314 (bind address 127.0.0.1) -Press Ctrl+C to stop -``` - -Live reload and `--navigateToChanged` between the servers work as expected. - - -## Translate Your Content +## Translate your content There are two ways to manage your content translations. Both ensure each page is assigned a language and is linked to its counterpart translations. -### Translation by filename +### Translation by file name Considering the following example: 1. `/content/about.en.md` -2. `/content/about.fr.md` +1. `/content/about.fr.md` The first file is assigned the English language and is linked to the second. The second file is assigned the French language and is linked to the first. -Their language is __assigned__ according to the language code added as a __suffix to the filename__. +Their language is __assigned__ according to the language code added as a __suffix to the file name__. -By having the same **path and base filename**, the content pieces are __linked__ together as translated pages. +By having the same **path and base file name**, the content pieces are __linked__ together as translated pages. -{{% note %}} -If a file has no language code, it will be assigned the default language. -{{% /note %}} +> [!note] +> If a file has no language code, it will be assigned the default language. ### Translation by content directory -This system uses different content directories for each of the languages. Each language's content directory is set using the `contentDir` param. +This system uses different content directories for each of the languages. Each language's `content` directory is set using the `contentDir` parameter. -{{< code-toggle file="hugo" >}} +{{< code-toggle file=hugo >}} languages: en: weight: 10 @@ -211,14 +53,14 @@ The value of `contentDir` can be any valid path -- even absolute path references Considering the following example in conjunction with the configuration above: 1. `/content/english/about.md` -2. `/content/french/about.md` +1. `/content/french/about.md` The first file is assigned the English language and is linked to the second. The second file is assigned the French language and is linked to the first. -Their language is __assigned__ according to the content directory they are __placed__ in. +Their language is __assigned__ according to the `content` directory they are __placed__ in. -By having the same **path and basename** (relative to their language content directory), the content pieces are __linked__ together as translated pages. +By having the same **path and basename** (relative to their language `content` directory), the content pieces are __linked__ together as translated pages. ### Bypassing default linking @@ -227,180 +69,89 @@ Any pages sharing the same `translationKey` set in front matter will be linked a Considering the following example: 1. `/content/about-us.en.md` -2. `/content/om.nn.md` -3. `/content/presentation/a-propos.fr.md` +1. `/content/om.nn.md` +1. `/content/presentation/a-propos.fr.md` -{{< code-toggle >}} +{{< code-toggle file=hugo >}} translationKey: "about" {{< /code-toggle >}} -By setting the `translationKey` front matter param to `about` in all three pages, they will be __linked__ as translated pages. +By setting the `translationKey` front matter parameter to `about` in all three pages, they will be __linked__ as translated pages. ### Localizing permalinks -Because paths and filenames are used to handle linking, all translated pages will share the same URL (apart from the language subdirectory). +Because paths and file names are used to handle linking, all translated pages will share the same URL (apart from the language subdirectory). To localize URLs: - For a regular page, set either [`slug`] or [`url`] in front matter - For a section page, set [`url`] in front matter -[`slug`]: /content-management/urls/#slug -[`url`]: /content-management/urls/#url - For example, a French translation can have its own localized slug. -{{< code-toggle file="content/about.fr.md" fm=true copy=false >}} +{{< code-toggle file=content/about.fr.md fm=true >}} title: A Propos slug: "a-propos" {{< /code-toggle >}} At render, Hugo will build both `/about/` and `/fr/a-propos/` without affecting the translation link. -### Page Bundles +### Page bundles -To avoid the burden of having to duplicate files, each Page Bundle inherits the resources of its linked translated pages' bundles except for the content files (Markdown files, HTML files etc...). +To avoid the burden of having to duplicate files, each Page Bundle inherits the resources of its linked translated pages' bundles except for the content files (Markdown files, HTML files etc.). Therefore, from within a template, the page will have access to the files from all linked pages' bundles. If, across the linked bundles, two or more files share the same basename, only one will be included and chosen as follows: -* File from current language bundle, if present. -* First file found across bundles by order of language `Weight`. +- File from current language bundle, if present. +- First file found across bundles by order of language `Weight`. -{{% note %}} -Page Bundle resources follow the same language assignment logic as content files, both by filename (`image.jpg`, `image.fr.jpg`) and by directory (`english/about/header.jpg`, `french/about/header.jpg`). -{{%/ note %}} +> [!note] +> Page Bundle resources follow the same language assignment logic as content files, both by file name (`image.jpg`, `image.fr.jpg`) and by directory (`english/about/header.jpg`, `french/about/header.jpg`). -## Reference the Translated Content +## Reference translated content To create a list of links to translated content, use a template similar to the following: -{{< code file="layouts/partials/i18nlist.html" >}} +```go-html-template {file="layouts/partials/i18nlist.html"} {{ if .IsTranslated }}

    {{ i18n "translations" }}

    {{ end }} -{{< /code >}} +``` -The above can be put in a `partial` (i.e., inside `layouts/partials/`) and included in any template, whether a [single content page][contenttemplate] or the [homepage]. It will not print anything if there are no translations for a given page. +The above can be put in a `partial` (i.e., inside `layouts/partials/`) and included in any template. It will not print anything if there are no translations for a given page. The above also uses the [`i18n` function][i18func] described in the next section. -### List All Available Languages +### List all available languages `.AllTranslations` on a `Page` can be used to list all translations, including the page itself. On the home page it can be used to build a language navigator: -{{< code file="layouts/partials/allLanguages.html" >}} +```go-html-template {file="layouts/partials/allLanguages.html"} -{{< /code >}} - -## Translation of Strings - -Hugo uses [go-i18n] to support string translations. [See the project's source repository][go-i18n-source] to find tools that will help you manage your translation workflows. - -Translations are collected from the `themes//i18n/` folder (built into the theme), as well as translations present in `i18n/` at the root of your project. In the `i18n`, the translations will be merged and take precedence over what is in the theme folder. Language files should be named according to [RFC 5646] with names such as `en-US.toml`, `fr.toml`, etc. - -Artificial languages with private use subtags as defined in [RFC 5646 § 2.2.7](https://datatracker.ietf.org/doc/html/rfc5646#section-2.2.7) are also supported. You may omit the `art-x-` prefix for brevity. For example: - -```text -art-x-hugolang -hugolang ``` -Private use subtags must not exceed 8 alphanumeric characters. +## Translation of strings -### Query basic translation - -From within your templates, use the `i18n` function like this: - -```go-html-template -{{ i18n "home" }} -``` - -The function will search for the `"home"` id: - -{{< code-toggle file="i18n/en-US" >}} -[home] -other = "Home" -{{< /code-toggle >}} - -The result will be - -```text -Home -``` - -### Query a flexible translation with variables - -Often you will want to use the page variables in the translation strings. To do so, pass the `.` context when calling `i18n`: - -```go-html-template -{{ i18n "wordCount" . }} -``` - -The function will pass the `.` context to the `"wordCount"` id: - -{{< code-toggle file="i18n/en-US" >}} -[wordCount] -other = "This article has {{ .WordCount }} words." -{{< /code-toggle >}} - -Assume `.WordCount` in the context has value is 101. The result will be: - -```text -This article has 101 words. -``` - -### Query a singular/plural translation - -In other to meet singular/plural requirement, you must pass a dictionary (map) with a numeric `.Count` property to the `i18n` function. The below example uses `.ReadingTime` variable which has a built-in `.Count` property. - -```go-html-template -{{ i18n "readingTime" .ReadingTime }} -``` - -The function will read `.Count` from `.ReadingTime` and evaluate whether the number is singular (`one`) or plural (`other`). After that, it will pass to `readingTime` id in `i18n/en-US.toml` file: - -{{< code-toggle file="i18n/en-US" >}} -[readingTime] -one = "One minute to read" -other = "{{ .Count }} minutes to read" -{{< /code-toggle >}} - -Assuming `.ReadingTime.Count` in the context has value is 525600. The result will be: - -```text -525600 minutes to read -``` - -If `.ReadingTime.Count` in the context has value is 1. The result is: - -```text -One minute to read -``` - -In case you need to pass a custom data: (`(dict "Count" numeric_value_only)` is minimum requirement) - -```go-html-template -{{ i18n "readingTime" (dict "Count" 25 "FirstArgument" true "SecondArgument" false "Etc" "so on, so far") }} -``` +See the [`lang.Translate`] template function. ## Localization The following localization examples assume your site's primary language is English, with translations to French and German. -{{< code-toggle file="hugo" >}} +{{< code-toggle file=hugo >}} defaultContentLanguage = 'en' [languages] @@ -423,7 +174,7 @@ weight = 3 With this front matter: -{{< code-toggle >}} +{{< code-toggle file=hugo >}} date = 2021-11-03T12:34:56+01:00 {{< /code-toggle >}} @@ -441,7 +192,7 @@ English|Wednesday, November 3, 2021 Français|mercredi 3 novembre 2021 Deutsch|Mittwoch, 3. November 2021 -See [time.Format] for details. +See [`time.Format`] for details. ### Currency @@ -484,7 +235,7 @@ See [lang.FormatNumber] and [lang.FormatNumberCustom] for details. With this template code: ```go-html-template -{{ 512.5032 | lang.FormatPercent 2 }} ---> 512.50% +{{ 512.5032 | lang.FormatPercent 2 }} ``` The rendered page displays: @@ -499,11 +250,83 @@ See [lang.FormatPercent] for details. ## Menus -Localization of menu entries depends on the how you define them: +Localization of menu entries depends on how you define them: - When you define menu entries [automatically] using the section pages menu, you must use translation tables to localize each entry. - When you define menu entries [in front matter], they are already localized based on the front matter itself. If the front matter values are insufficient, use translation tables to localize each entry. -- When you define menu entries [in site configuration], you can (a) use translation tables, or (b) create language-specific menu entries under each language key. +- When you define menu entries [in site configuration], you must create language-specific menu entries under each language key. If the names of the menu entries are insufficient, use translation tables to localize each entry. + +### Create language-specific menu entries + +#### Method 1 -- Use a single configuration file + +For a simple menu with a small number of entries, use a single configuration file. For example: + +{{< code-toggle file=hugo >}} +[languages.de] +languageCode = 'de-DE' +languageName = 'Deutsch' +weight = 1 + +[[languages.de.menus.main]] +name = 'Produkte' +pageRef = '/products' +weight = 10 + +[[languages.de.menus.main]] +name = 'Leistungen' +pageRef = '/services' +weight = 20 + +[languages.en] +languageCode = 'en-US' +languageName = 'English' +weight = 2 + +[[languages.en.menus.main]] +name = 'Products' +pageRef = '/products' +weight = 10 + +[[languages.en.menus.main]] +name = 'Services' +pageRef = '/services' +weight = 20 +{{< /code-toggle >}} + +#### Method 2 -- Use a configuration directory + +With a more complex menu structure, create a [configuration directory] and split the menu entries into multiple files, one file per language. For example: + +```text +config/ +└── _default/ + ├── menus.de.toml + ├── menus.en.toml + └── hugo.toml +``` + +{{< code-toggle file=config/_default/menus.de >}} +[[main]] +name = 'Produkte' +pageRef = '/products' +weight = 10 +[[main]] +name = 'Leistungen' +pageRef = '/services' +weight = 20 +{{< /code-toggle >}} + +{{< code-toggle file=config/_default/menus.en >}} +[[main]] +name = 'Products' +pageRef = '/products' +weight = 10 +[[main]] +name = 'Services' +pageRef = '/services' +weight = 20 +{{< /code-toggle >}} ### Use translation tables @@ -522,13 +345,13 @@ The `identifier` depends on how you define menu entries: For example, if you define menu entries in site configuration: -{{< code-toggle file="hugo" copy=false >}} -[[menu.main]] +{{< code-toggle file=hugo >}} +[[menus.main]] identifier = 'products' name = 'Products' pageRef = '/products' weight = 10 -[[menu.main]] +[[menus.main]] identifier = 'services' name = 'Services' pageRef = '/services' @@ -537,116 +360,70 @@ For example, if you define menu entries in site configuration: Create corresponding entries in the translation tables: -{{< code-toggle file="i18n/de" copy=false >}} +{{< code-toggle file=i18n/de >}} products = 'Produkte' services = 'Leistungen' {{< / code-toggle >}} -[example menu template]: /templates/menu-templates/#example -[automatically]: /content-management/menus/#define-automatically -[in front matter]: /content-management/menus/#define-in-front-matter -[in site configuration]: /content-management/menus/#define-in-site-configuration - -### Create language-specific menu entries - -For example: - -{{< code-toggle file="hugo" copy=false >}} -[languages.de] -languageCode = 'de-DE' -languageName = 'Deutsch' -weight = 1 - -[[languages.de.menu.main]] -name = 'Produkte' -pageRef = '/products' -weight = 10 - -[[languages.de.menu.main]] -name = 'Leistungen' -pageRef = '/services' -weight = 20 - -[languages.en] -languageCode = 'en-US' -languageName = 'English' -weight = 2 - -[[languages.en.menu.main]] -name = 'Products' -pageRef = '/products' -weight = 10 - -[[languages.en.menu.main]] -name = 'Services' -pageRef = '/services' -weight = 20 -{{< /code-toggle >}} - -For a simple menu with two languages, these menu entries are easy to create and maintain. For a larger menu, or with more than two languages, using translation tables as described above is preferable. - -## Missing Translations +## Missing translations If a string does not have a translation for the current language, Hugo will use the value from the default language. If no default value is set, an empty string will be shown. While translating a Hugo website, it can be handy to have a visual indicator of missing translations. The [`enableMissingTranslationPlaceholders` configuration option][config] will flag all untranslated strings with the placeholder `[i18n] identifier`, where `identifier` is the id of the missing translation. -{{% note %}} -Hugo will generate your website with these missing translation placeholders. It might not be suitable for production environments. -{{% /note %}} +> [!note] +> Hugo will generate your website with these missing translation placeholders. It might not be suitable for production environments. For merging of content from other languages (i.e. missing content translations), see [lang.Merge]. To track down missing translation strings, run Hugo with the `--printI18nWarnings` flag: -```bash +```sh hugo --printI18nWarnings | grep i18n i18n|MISSING_TRANSLATION|en|wordCount ``` -## Multilingual Themes support +## Multilingual themes support To support Multilingual mode in your themes, some considerations must be taken for the URLs in the templates. If there is more than one language, URLs must meet the following criteria: -* Come from the built-in `.Permalink` or `.RelPermalink` -* Be constructed with the [`relLangURL` template function][rellangurl] or the [`absLangURL` template function][abslangurl] **OR** be prefixed with `{{ .LanguagePrefix }}` +- Come from the built-in `.Permalink` or `.RelPermalink` +- Be constructed with the [`relLangURL`] or [`absLangURL`] template function, or be prefixed with `{{ .LanguagePrefix }}` -If there is more than one language defined, the `LanguagePrefix` variable will equal `/en` (or whatever your `CurrentLanguage` is). If not enabled, it will be an empty string (and is therefore harmless for single-language Hugo websites). +If there is more than one language defined, the `LanguagePrefix` method will return `/en` (or whatever the current language is). If not enabled, it will be an empty string (and is therefore harmless for single-language Hugo websites). - -## Generate multilingual content with `hugo new` +## Generate multilingual content with `hugo new content` If you organize content with translations in the same directory: -```text -hugo new post/test.en.md -hugo new post/test.de.md +```sh +hugo new content post/test.en.md +hugo new content post/test.de.md ``` If you organize content with translations in different directories: -```text -hugo new content/en/post/test.md -hugo new content/de/post/test.md +```sh +hugo new content content/en/post/test.md +hugo new content content/de/post/test.md ``` -[abslangurl]: /functions/abslangurl -[config]: /getting-started/configuration/ -[contenttemplate]: /templates/single-page-templates/ -[go-i18n-source]: https://github.com/nicksnyder/go-i18n -[go-i18n]: https://github.com/nicksnyder/go-i18n -[homepage]: /templates/homepage/ -[Hugo Multilingual Part 1: Content translation]: https://regisphilibert.com/blog/2018/08/hugo-multilingual-part-1-managing-content-translation/ -[i18func]: /functions/i18n/ -[lang.FormatAccounting]: /functions/lang -[lang.FormatCurrency]: /functions/lang -[lang.FormatNumber]: /functions/lang -[lang.FormatNumberCustom]: /functions/lang -[lang.FormatPercent]: /functions/lang -[lang.Merge]: /functions/lang.merge/ -[menus]: /content-management/menus/ -[OS environment]: /getting-started/configuration/#configure-with-environment-variables -[rellangurl]: /functions/rellangurl -[RFC 5646]: https://tools.ietf.org/html/rfc5646 -[single page templates]: /templates/single-page-templates/ -[time.Format]: /functions/dateformat +[`absLangURL`]: /functions/urls/abslangurl/ +[`lang.Translate`]: /functions/lang/translate +[`relLangURL`]: /functions/urls/rellangurl/ +[`slug`]: /content-management/urls/#slug +[`time.Format`]: /functions/time/format/ +[`url`]: /content-management/urls/#url +[automatically]: /content-management/menus/#define-automatically +[config]: /configuration/ +[configuration directory]: /configuration/introduction/#configuration-directory +[example menu template]: /templates/menu/#example +[i18func]: /functions/lang/translate/ +[in front matter]: /content-management/menus/#define-in-front-matter +[in site configuration]: /content-management/menus/#define-in-site-configuration +[lang.FormatAccounting]: /functions/lang/formataccounting/ +[lang.FormatCurrency]: /functions/lang/formatcurrency/ +[lang.FormatNumber]: /functions/lang/formatnumber/ +[lang.FormatNumberCustom]: /functions/lang/formatnumbercustom/ +[lang.FormatPercent]: /functions/lang/formatpercent/ +[lang.Merge]: /functions/lang/merge/ diff --git a/docs/content/en/content-management/organization/1-featured-content-bundles.png b/docs/content/en/content-management/organization/1-featured-content-bundles.png deleted file mode 100644 index 501e671e2..000000000 Binary files a/docs/content/en/content-management/organization/1-featured-content-bundles.png and /dev/null differ diff --git a/docs/content/en/content-management/organization/index.md b/docs/content/en/content-management/organization/index.md index efa355ddc..a7682bfad 100644 --- a/docs/content/en/content-management/organization/index.md +++ b/docs/content/en/content-management/organization/index.md @@ -1,35 +1,42 @@ --- -title: Content Organization +title: Content organization linkTitle: Organization description: Hugo assumes that the same structure that works to organize your source content is used to organize the rendered site. -categories: [content management,fundamentals] -keywords: [sections,content,organization,bundle,resources] -menu: - docs: - parent: content-management - weight: 20 -toc: true -weight: 20 +categories: [] +keywords: [] aliases: [/content/sections/] --- -## Page Bundles +## Page bundles Hugo `0.32` announced page-relative images and other resources packaged into `Page Bundles`. These terms are connected, and you also need to read about [Page Resources](/content-management/page-resources) and [Image Processing](/content-management/image-processing) to get the full picture. -{{< imgproc 1-featured Resize "300x" >}} -The illustration shows three bundles. Note that the home page bundle cannot contain other content pages, although other files (images etc.) are allowed. -{{< /imgproc >}} +```text +content/ +├── blog/ +│ ├── hugo-is-cool/ +│ │ ├── images/ +│ │ │ ├── funnier-cat.jpg +│ │ │ └── funny-cat.jpg +│ │ ├── cats-info.md +│ │ └── index.md +│ ├── posts/ +│ │ ├── post1.md +│ │ └── post2.md +│ ├── 1-landscape.jpg +│ ├── 2-sunset.jpg +│ ├── _index.md +│ ├── content-1.md +│ └── content-2.md +├── 1-logo.png +└── _index.md +``` +The file tree above shows three bundles. Note that the home page bundle cannot contain other content pages, although other files (images etc.) are allowed. -{{% note %}} -The bundle documentation is a **work in progress**. We will publish more comprehensive docs about this soon. -{{% /note %}} - - -## Organization of Content Source +## Organization of content source In Hugo, your content should be organized in a manner that reflects the rendered website. @@ -41,39 +48,36 @@ Without any additional configuration, the following will automatically work: . └── content └── about - | └── index.md // <- https://example.com/about/ + | └── index.md // <- https://example.org/about/ ├── posts - | ├── firstpost.md // <- https://example.com/posts/firstpost/ + | ├── firstpost.md // <- https://example.org/posts/firstpost/ | ├── happy - | | └── ness.md // <- https://example.com/posts/happy/ness/ - | └── secondpost.md // <- https://example.com/posts/secondpost/ + | | └── ness.md // <- https://example.org/posts/happy/ness/ + | └── secondpost.md // <- https://example.org/posts/secondpost/ └── quote - ├── first.md // <- https://example.com/quote/first/ - └── second.md // <- https://example.com/quote/second/ + ├── first.md // <- https://example.org/quote/first/ + └── second.md // <- https://example.org/quote/second/ ``` -## Path Breakdown in Hugo +## Path breakdown in Hugo +The following demonstrates the relationships between your content organization and the output URL structure for your Hugo website when it renders. These examples assume you are [using pretty URLs][pretty], which is the default behavior for Hugo. The examples also assume a key-value of `baseURL = "https://example.org/"` in your [site's configuration file][config]. -The following demonstrates the relationships between your content organization and the output URL structure for your Hugo website when it renders. These examples assume you are [using pretty URLs][pretty], which is the default behavior for Hugo. The examples also assume a key-value of `baseURL = "https://example.com"` in your [site's configuration file][config]. +### Index pages: `_index.md` -### Index Pages: `_index.md` +`_index.md` has a special role in Hugo. It allows you to add front matter and content to `home`, `section`, `taxonomy`, and `term` pages. -`_index.md` has a special role in Hugo. It allows you to add front matter and content to your [list templates][lists]. These templates include those for [section templates], [taxonomy templates], [taxonomy terms templates], and your [homepage template]. - -{{% note %}} -**Tip:** You can get a reference to the content and metadata in `_index.md` using the [`.Site.GetPage` function](/functions/getpage/). -{{% /note %}} - -You can create one `_index.md` for your homepage and one in each of your content sections, taxonomies, and taxonomy terms. The following shows typical placement of an `_index.md` that would contain content and front matter for a `posts` section list page on a Hugo website: +> [!note] +> Access the content and metadata within an `_index.md` file by invoking the `GetPage` method on a `Site` or `Page` object. +You can create one `_index.md` for your home page and one in each of your content sections, taxonomies, and terms. The following shows typical placement of an `_index.md` that would contain content and front matter for a `posts` section list page on a Hugo website: ```txt . url . ⊢--^-⊣ . path slug . ⊢--^-⊣⊢---^---⊣ -. filepath +. file path . ⊢------^------⊣ content/posts/_index.md ``` @@ -88,16 +92,14 @@ At build, this will output to the following destination with the associated valu ⊢--------^---------⊣⊢-^-⊣ permalink ⊢----------^-------------⊣ -https://example.com/posts/index.html +https://example.org/posts/index.html ``` The [sections] can be nested as deeply as you want. The important thing to understand is that to make the section tree fully navigational, at least the lower-most section must include a content file. (i.e. `_index.md`). +### Single pages in sections -### Single Pages in Sections - -Single content files in each of your sections will be rendered as [single page templates][singles]. Here is an example of a single `post` within `posts`: - +Single content files in each of your sections will be rendered by a [single template]. Here is an example of a single `post` within `posts`: ```txt path ("posts/my-first-hugo-post.md") @@ -117,11 +119,10 @@ When Hugo builds your site, the content will be output to the following destinat ⊢--------^--------⊣⊢-^--⊣⊢-------^---------⊣ permalink ⊢--------------------^---------------------⊣ -https://example.com/posts/my-first-hugo-post/index.html +https://example.org/posts/my-first-hugo-post/index.html ``` - -## Paths Explained +## Paths explained The following concepts provide more insight into the relationship between your project's organization and the default Hugo behavior when building output for the website. @@ -135,27 +136,16 @@ The `slug` is the last segment of the URL path, defined by the file name and opt ### `path` -A content's `path` is determined by the section's path to the file. The file `path` +A content's `path` is determined by the section's path to the file. The file `path`: -* is based on the path to the content's location AND -* does not include the slug +- Is based on the path to the content's location AND +- Does not include the slug ### `url` The `url` is the entire URL path, defined by the file path and optionally overridden by a `url` value in front matter. See [URL Management](/content-management/urls/#slug) for details. -[config]: /getting-started/configuration/ -[formats]: /content-management/formats/ -[front matter]: /content-management/front-matter/ -[getpage]: /functions/getpage/ -[homepage template]: /templates/homepage/ -[homepage]: /templates/homepage/ -[lists]: /templates/lists/ +[config]: /configuration/ [pretty]: /content-management/urls/#appearance -[section templates]: /templates/section-templates/ [sections]: /content-management/sections/ -[singles]: /templates/single-page-templates/ -[taxonomy templates]: /templates/taxonomy-templates/ -[taxonomy terms templates]: /templates/taxonomy-templates/ -[types]: /content-management/types/ -[urls]: /content-management/urls/ +[single template]: /templates/types/#single diff --git a/docs/content/en/content-management/page-bundles.md b/docs/content/en/content-management/page-bundles.md index 2a5147c21..f6a5cf771 100644 --- a/docs/content/en/content-management/page-bundles.md +++ b/docs/content/en/content-management/page-bundles.md @@ -1,186 +1,145 @@ --- -title: Page Bundles -description: Content organization using Page Bundles -keywords: [page, bundle, leaf, branch] -categories: [content management] -menu : - docs: - parent: content-management - weight: 30 -toc: true -weight: 30 +title: Page bundles +description: Use page bundles to logically associate one or more resources with content. +categories: [] +keywords: [] --- -Page Bundles are a way to group [Page Resources](/content-management/page-resources/). +## Introduction -A Page Bundle can be one of: +A page bundle is a directory that encapsulates both content and associated resources. -- Leaf Bundle (leaf means it has no children) -- Branch Bundle (home page, section, taxonomy terms, taxonomy list) +By way of example, this site has an "about" page and a "privacy" page: -| | Leaf Bundle | Branch Bundle | -|-------------------------------------|----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Usage | Collection of content and attachments for single pages | Collection of attachments for section pages (home page, section, taxonomy terms, taxonomy list) | -| Index filename | `index.md` [^fn:1] | `_index.md` [^fn:1] | -| Allowed Resources | Page and non-page (like images, PDF, etc.) types | Only non-page (like images, PDF, etc.) types | -| Where can the Resources live? | At any directory level within the leaf bundle directory. | Only in the directory level **of** the branch bundle directory i.e. the directory containing the `_index.md` ([ref](https://discourse.gohugo.io/t/question-about-content-folder-structure/11822/4?u=kaushalmodi)). | -| Layout type | `single` | `list` | -| Nesting | Does not allow nesting of more bundles under it | Allows nesting of leaf or branch bundles under it | -| Example | `content/posts/my-post/index.md` | `content/posts/_index.md` | -| Content from non-index page files... | Accessed only as page resources | Accessed only as regular pages | +```text +content/ +├── about/ +│ ├── index.md +│ └── welcome.jpg +└── privacy.md +``` +The "about" page is a page bundle. It logically associates a resource with content by bundling them together. Resources within a page bundle are [page resources], accessible with the [`Resources`] method on the `Page` object. -## Leaf Bundles {#leaf-bundles} +Page bundles are either _leaf bundles_ or _branch bundles_. -A _Leaf Bundle_ is a directory at any hierarchy within the `content/` -directory, that contains an **`index.md`** file. +leaf bundle +: A _leaf bundle_ is a directory that contains an `index.md` file and zero or more resources. Analogous to a physical leaf, a leaf bundle is at the end of a branch. It has no descendants. -### Examples of Leaf Bundle organization {#examples-of-leaf-bundle-organization} +branch bundle +: A _branch bundle_ is a directory that contains an `_index.md` file and zero or more resources. Analogous to a physical branch, a branch bundle may have descendants including leaf bundles and other branch bundles. Top-level directories with or without `_index.md` files are also branch bundles. This includes the home page. + +> [!note] +> In the definitions above and the examples below, the extension of the index file depends on the [content format](g). For example, use `index.md` for Markdown content, `index.html` for HTML content, `index.adoc` for AsciiDoc content, etc. + +## Comparison + +Page bundle characteristics vary by bundle type. + +| | Leaf bundle | Branch bundle | +|---------------------|---------------------------------------------------------|---------------------------------------------------------| +| Index file | `index.md` | `_index.md` | +| Example | `content/about/index.md` | `content/posts/_index.md ` | +| [Page kinds](g) | `page` | `home`, `section`, `taxonomy`, or `term` | +| Template types | [single] | [home], [section], [taxonomy], or [term] | +| Descendant pages | None | Zero or more | +| Resource location | Adjacent to the index file or in a nested subdirectory | Same as a leaf bundles, but excludes descendant bundles | +| [Resource types](g) | `page`, `image`, `video`, etc. | all but `page` | + +Files with [resource type](g) `page` include content written in Markdown, HTML, AsciiDoc, Pandoc, reStructuredText, and Emacs Org Mode. In a leaf bundle, excluding the index file, these files are only accessible as page resources. In a branch bundle, these files are only accessible as content pages. + +## Leaf bundles + +A _leaf bundle_ is a directory that contains an `index.md` file and zero or more resources. Analogous to a physical leaf, a leaf bundle is at the end of a branch. It has no descendants. ```text content/ ├── about -│ ├── index.md +│ └── index.md ├── posts │ ├── my-post -│ │ ├── content1.md -│ │ ├── content2.md -│ │ ├── image1.jpg -│ │ ├── image2.png +│ │ ├── content-1.md +│ │ ├── content-2.md +│ │ ├── image-1.jpg +│ │ ├── image-2.png │ │ └── index.md │ └── my-other-post -│    └── index.md -│ +│ └── index.md └── another-section - ├── .. -    └── not-a-leaf-bundle - ├── .. -    └── another-leaf-bundle -    └── index.md + ├── foo.md + └── not-a-leaf-bundle + ├── bar.md + └── another-leaf-bundle + └── index.md ``` -In the above example `content/` directory, there are four leaf -bundles: +There are four leaf bundles in the example above: -`about` -: This leaf bundle is at the root level (directly under - `content` directory) and has only the `index.md`. +about +: This leaf bundle does not contain any page resources. -`my-post` -: This leaf bundle has the `index.md`, two other content - Markdown files and two image files. +my-post +: This leaf bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`. -- image1, image2: -These images are page resources of `my-post` - and only available in `my-post/index.md` resources. + - content-1, content-2 -- content1, content2: -These content files are page resources of `my-post` - and only available in `my-post/index.md` resources. - They will **not** be rendered as individual pages. + These are resources of resource type `page`, accessible via the [`Resources`] method on the `Page` object. Hugo will not render these as individual pages. -`my-other-post` -: This leaf bundle has only the `index.md`. + - image-1, image-2 -`another-leaf-bundle` -: This leaf bundle is nested under couple of - directories. This bundle also has only the `index.md`. + These are resources of resource type `image`, accessible via the `Resources` method on the `Page` object -{{% note %}} -The hierarchy depth at which a leaf bundle is created does not matter, -as long as it is not inside another **leaf** bundle. -{{% /note %}} +my-other-post +: This leaf bundle does not contain any page resources. +another-leaf-bundle +: This leaf bundle does not contain any page resources. -### Headless Bundle {#headless-bundle} +> [!note] +> Create leaf bundles at any depth within the `content` directory, but a leaf bundle may not contain another bundle. Leaf bundles do not have descendants. -A headless bundle is a bundle that is configured to not get published -anywhere: +## Branch bundles -- It will have no `Permalink` and no rendered HTML in `public/`. -- It will not be part of `.Site.RegularPages`, etc. - -But you can get it by `.Site.GetPage`. Here is an example: - -```go-html-template -{{ $headless := .Site.GetPage "/some-headless-bundle" }} -{{ $reusablePages := $headless.Resources.Match "author*" }} -

    Authors

    -{{ range $reusablePages }} -

    {{ .Title }}

    - {{ .Content }} -{{ end }} -``` - -_In this example, we are assuming the `some-headless-bundle` to be a headless - bundle containing one or more **page** resources whose `.Name` matches - `"author*"`._ - -Explanation of the above example: - -1. Get the `some-headless-bundle` Page "object". -2. Collect a _slice_ of resources in this _Page Bundle_ that matches - `"author*"` using `.Resources.Match`. -3. Loop through that _slice_ of nested pages, and output their `.Title` and - `.Content`. - ---- - -A leaf bundle can be made headless by adding below in the Front Matter -(in the `index.md`): - -{{< code-toggle file="content/headless/index.md" fm=true copy=false >}} -headless = true -{{< /code-toggle >}} - -There are many use cases of such headless page bundles: - -- Shared media galleries -- Reusable page content "snippets" - -## Branch Bundles {#branch-bundles} - -A _Branch Bundle_ is any directory at any hierarchy within the -`content/` directory, that contains at least an **`_index.md`** file. - -This `_index.md` can also be directly under the `content/` directory. - -{{% note %}} -Here `md` (markdown) is used just as an example. You can use any file -type as a content resource as long as it is a content type recognized by Hugo. -{{% /note %}} - - -### Examples of Branch Bundle organization {#examples-of-branch-bundle-organization} +A _branch bundle_ is a directory that contains an `_index.md` file and zero or more resources. Analogous to a physical branch, a branch bundle may have descendants including leaf bundles and other branch bundles. Top-level directories with or without `_index.md` files are also branch bundles. This includes the home page. ```text content/ -├── branch-bundle-1 -│   ├── branch-content1.md -│   ├── branch-content2.md -│   ├── image1.jpg -│   ├── image2.png -│   └── _index.md -└── branch-bundle-2 - ├── _index.md - └── a-leaf-bundle - └── index.md +├── branch-bundle-1/ +│ ├── _index.md +│ ├── content-1.md +│ ├── content-2.md +│ ├── image-1.jpg +│ └── image-2.png +├── branch-bundle-2/ +│ ├── a-leaf-bundle/ +│ │ └── index.md +│ └── _index.md +└── _index.md ``` -In the above example `content/` directory, there are two branch -bundles (and a leaf bundle): +There are three branch bundles in the example above: -`branch-bundle-1` -: This branch bundle has the `_index.md`, two - other content Markdown files and two image files. +home page +: This branch bundle contains an index file, two descendant branch bundles, and no resources. -`branch-bundle-2` -: This branch bundle has the `_index.md` and a - nested leaf bundle. +branch-bundle-1 +: This branch bundle contains an index file, two resources of [resource type](g) `page`, and two resources of resource type `image`. -{{% note %}} -The hierarchy depth at which a branch bundle is created does not -matter. -{{% /note %}} +branch-bundle-2 +: This branch bundle contains an index file and a leaf bundle. -[^fn:1]: The `.md` extension is just an example. The extension can be `.html`, `.json` or any valid MIME type. +> [!note] +> Create branch bundles at any depth within the `content` directory. Branch bundles may have descendants. + +## Headless bundles + +Use [build options] in front matter to create an unpublished leaf or branch bundle whose content and resources you can include in other pages. + +[`Resources`]: /methods/page/resources/ +[build options]: /content-management/build-options/ +[home]: /templates/types/#home +[page resources]: /content-management/page-resources/ +[section]: /templates/types/#section +[single]: /templates/types/#single +[taxonomy]: /templates/types/#taxonomy +[term]: /templates/types/#term diff --git a/docs/content/en/content-management/page-resources.md b/docs/content/en/content-management/page-resources.md index 54494c2e1..204ca5301 100644 --- a/docs/content/en/content-management/page-resources.md +++ b/docs/content/en/content-management/page-resources.md @@ -1,17 +1,12 @@ --- -title: Page Resources -description: Page resources -- images, other pages, documents, etc. -- have page-relative URLs and their own metadata. -categories: [content management] -keywords: [bundle,content,resources] -menu: - docs: - parent: content-management - weight: 80 -toc: true -weight: 80 +title: Page resources +description: Use page resources to logically associate assets with a page. +categories: [] +keywords: [] --- + Page resources are only accessible from [page bundles](/content-management/page-bundles), those directories with `index.md` or -`_index.md` files at their root. Page resources are only available to the +`_index.md` files at their root. Page resources are only available to the page with which they are bundled. In this example, `first-post` is a page bundle with access to 10 page resources including audio, data, documents, images, and video. Although `second-post` is also a page bundle, it has no page resources and is unable to directly access the page resources associated with `first-post`. @@ -36,109 +31,101 @@ content └── index.md (root of page bundle) ``` -## Properties +## Examples -ResourceType -: The main type of the resource's [Media Type](/templates/output-formats/#media-types). For example, a file of MIME type `image/jpeg` has the ResourceType `image`. A `Page` will have `ResourceType` with value `page`. +Use any of these methods on a `Page` object to capture page resources: -Name -: Default value is the filename (relative to the owning page). Can be set in front matter. + - [`Resources.ByType`] + - [`Resources.Get`] + - [`Resources.GetMatch`] + - [`Resources.Match`] -Title -: Default value is the same as `.Name`. Can be set in front matter. + Once you have captured a resource, use any of the applicable [`Resource`] methods to return a value or perform an action. -Permalink -: The absolute URL to the resource. Resources of type `page` will have no value. +The following examples assume this content structure: -RelPermalink -: The relative URL to the resource. Resources of type `page` will have no value. +```text +content/ +└── example/ + ├── data/ + │ └── books.json <-- page resource + ├── images/ + │ ├── a.jpg <-- page resource + │ └── b.jpg <-- page resource + ├── snippets/ + │ └── text.md <-- page resource + └── index.md +``` -Content -: The content of the resource itself. For most resources, this returns a string -with the contents of the file. Use this to create inline resources. +Render a single image, and throw an error if the file does not exist: ```go-html-template -{{ with .Resources.GetMatch "script.js" }} - -{{ end }} - -{{ with .Resources.GetMatch "style.css" }} - -{{ end }} - -{{ with .Resources.GetMatch "img.png" }} - +{{ $path := "images/a.jpg" }} +{{ with .Resources.Get $path }} + +{{ else }} + {{ errorf "Unable to get page resource %q" $path }} {{ end }} ``` -MediaType -: The MIME type of the resource, such as `image/jpeg`. - -MediaType.MainType -: The main type of the resource's MIME type. For example, a file of MIME type `application/pdf` has for MainType `application`. - -MediaType.SubType -: The subtype of the resource's MIME type. For example, a file of MIME type `application/pdf` has for SubType `pdf`. Note that this is not the same as the file extension. For example, Microsoft PowerPoint files (`.ppt`) have a subtype of `vnd.ms-powerpoint`. - -MediaType.Suffixes -: A slice of possible suffixes for the resource's MIME type. - -## Methods - -ByType -: Returns the page resources of the given type. +Render all images, resized to 300 px wide: ```go-html-template -{{ .Resources.ByType "image" }} +{{ range .Resources.ByType "image" }} + {{ with .Resize "300x" }} + + {{ end }} +{{ end }} ``` -Match -: Returns all the page resources (as a slice) whose `Name` matches the given Glob pattern ([examples](https://github.com/gobwas/glob/blob/master/readme.md)). The matching is case-insensitive. + +Render the markdown snippet: ```go-html-template -{{ .Resources.Match "images/*" }} +{{ with .Resources.Get "snippets/text.md" }} + {{ .Content }} +{{ end }} ``` -GetMatch -: Same as `Match` but will return the first match. - -### Pattern Matching - -```go -// Using Match/GetMatch to find this images/sunset.jpg ? -.Resources.Match "images/sun*" ✅ -.Resources.Match "**/sunset.jpg" ✅ -.Resources.Match "images/*.jpg" ✅ -.Resources.Match "**.jpg" ✅ -.Resources.Match "*" 🚫 -.Resources.Match "sunset.jpg" 🚫 -.Resources.Match "*sunset.jpg" 🚫 +List the titles in the data file, and throw an error if the file does not exist. +```go-html-template +{{ $path := "data/books.json" }} +{{ with .Resources.Get $path }} + {{ with . | transform.Unmarshal }} +

    Books:

    +
      + {{ range . }} +
    • {{ .title }}
    • + {{ end }} +
    + {{ end }} +{{ else }} + {{ errorf "Unable to get page resource %q" $path }} +{{ end }} ``` -## Page Resources Metadata +## Metadata The page resources' metadata is managed from the corresponding page's front matter with an array/table parameter named `resources`. You can batch assign values using [wildcards](https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm). -{{% note %}} -Resources of type `page` get `Title` etc. from their own front matter. -{{% /note %}} +> [!note] +> Resources of type `page` get `Title` etc. from their own front matter. name -: Sets the value returned in `Name`. +: (`string`) Sets the value returned in `Name`. -{{% note %}} -The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources. -{{% /note %}} +> [!note] +> The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources. title -: Sets the value returned in `Title` +: (`string`) Sets the value returned in `Title` params -: A map of custom key/values. +: (`map`) A map of custom key-value pairs. ### Resources metadata example -{{< code-toggle copy=false >}} +{{< code-toggle file=content/example.md fm=true >}} title: Application date : 2018-01-25 resources : @@ -172,9 +159,8 @@ From the example above: - All `PDF` files will get a new `Name`. The `name` parameter contains a special placeholder [`:counter`](#the-counter-placeholder-in-name-and-title), so the `Name` will be `pdf-file-1`, `pdf-file-2`, `pdf-file-3`. - Every docx in the bundle will receive the `word` icon. -{{% note %}} -The __order matters__ --- Only the **first set** values of the `title`, `name` and `params`-**keys** will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule. -{{% /note %}} +> [!note] +> The order matters; only the first set values of the `title`, `name` and `params` keys will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule. ### The `:counter` placeholder in `name` and `title` @@ -184,7 +170,8 @@ The counter starts at 1 the first time they are used in either `name` or `title` For example, if a bundle has the resources `photo_specs.pdf`, `other_specs.pdf`, `guide.pdf` and `checklist.pdf`, and the front matter has specified the `resources` as: -{{< code-toggle copy=false >}} +{{< code-toggle file=content/inspections/engine/index.md fm=true >}} +title = 'Engine inspections' [[resources]] src = "*specs.pdf" title = "Specification #:counter" @@ -201,3 +188,110 @@ the `Name` and `Title` will be assigned to the resource files as follows: | guide.pdf | `"pdf-file-2.pdf` | `"guide.pdf"` | | other\_specs.pdf | `"pdf-file-3.pdf` | `"Specification #1"` | | photo\_specs.pdf | `"pdf-file-4.pdf` | `"Specification #2"` | + +## Multilingual + +{{< new-in 0.123.0 />}} + +By default, with a multilingual single-host site, Hugo does not duplicate shared page resources when building the site. + +> [!note] +> This behavior is limited to Markdown content. Shared page resources for other [content formats] are copied into each language bundle. + +Consider this site configuration: + +{{< code-toggle file=hugo >}} +defaultContentLanguage = 'de' +defaultContentLanguageInSubdir = true + +[languages.de] +languageCode = 'de-DE' +languageName = 'Deutsch' +weight = 1 + +[languages.en] +languageCode = 'en-US' +languageName = 'English' +weight = 2 +{{< /code-toggle >}} + +And this content: + +```text +content/ +└── my-bundle/ + ├── a.jpg <-- shared page resource + ├── b.jpg <-- shared page resource + ├── c.de.jpg + ├── c.en.jpg + ├── index.de.md + └── index.en.md +``` + +With v0.122.0 and earlier, Hugo duplicated the shared page resources, creating copies for each language: + +```text +public/ +├── de/ +│ ├── my-bundle/ +│ │ ├── a.jpg <-- shared page resource +│ │ ├── b.jpg <-- shared page resource +│ │ ├── c.de.jpg +│ │ └── index.html +│ └── index.html +├── en/ +│ ├── my-bundle/ +│ │ ├── a.jpg <-- shared page resource (duplicate) +│ │ ├── b.jpg <-- shared page resource (duplicate) +│ │ ├── c.en.jpg +│ │ └── index.html +│ └── index.html +└── index.html + +``` + +With v0.123.0 and later, Hugo places the shared resources in the page bundle for the default content language: + +```text +public/ +├── de/ +│ ├── my-bundle/ +│ │ ├── a.jpg <-- shared page resource +│ │ ├── b.jpg <-- shared page resource +│ │ ├── c.de.jpg +│ │ └── index.html +│ └── index.html +├── en/ +│ ├── my-bundle/ +│ │ ├── c.en.jpg +│ │ └── index.html +│ └── index.html +└── index.html +``` + +This approach reduces build times, storage requirements, bandwidth consumption, and deployment times, ultimately reducing cost. + +> [!note] +> To resolve Markdown link and image destinations to the correct location, you must use link and image render hooks that capture the page resource with the [`Resources.Get`] method, and then invoke its [`RelPermalink`] method. +> +> By default, with multilingual single-host sites, Hugo enables its [embedded link render hook] and [embedded image render hook] to resolve Markdown link and image destinations. +> +> You may override the embedded render hooks as needed, provided they capture the resource as described above. + +Although duplicating shared page resources is inefficient, you can enable this feature in your site configuration if desired: + +{{< code-toggle file=hugo >}} +[markup.goldmark] +duplicateResourceFiles = true +{{< /code-toggle >}} + +[`RelPermalink`]: /methods/resource/relpermalink/ +[`Resource`]: /methods/resource +[`Resources.ByType`]: /methods/page/resources#bytype +[`Resources.Get`]: /methods/page/resources#get +[`Resources.Get`]: /methods/page/resources/#get +[`Resources.GetMatch`]: /methods/page/resources#getmatch +[`Resources.Match`]: /methods/page/resources#match +[content formats]: /content-management/formats/ +[embedded image render hook]: /render-hooks/images/#default +[embedded link render hook]: /render-hooks/links/#default diff --git a/docs/content/en/content-management/related-content.md b/docs/content/en/content-management/related-content.md new file mode 100644 index 000000000..d7b18dab0 --- /dev/null +++ b/docs/content/en/content-management/related-content.md @@ -0,0 +1,102 @@ +--- +title: Related content +description: List related content in "See Also" sections. +categories: [] +keywords: [] +aliases: [/content/related/,/related/,/content-management/related/] +--- + +Hugo uses a set of factors to identify a page's related content based on front matter parameters. This can be tuned to the desired set of indices and parameters or left to Hugo's default [related content configuration](/configuration/related-content/). + +## List related content + +To list up to 5 related pages (which share the same _date_ or _keyword_ parameters) is as simple as including something similar to this partial in your template: + +```go-html-template {file="layouts/partials/related.html" copy=true} +{{ with site.RegularPages.Related . | first 5 }} +

    Related content:

    + +{{ end }} +``` + +The `Related` method takes one argument which may be a `Page` or an options map. The options map has these options: + +indices +: (`slice`) The indices to search within. + +document +: (`page`) The page for which to find related content. Required when specifying an options map. + +namedSlices +: (`slice`) The keywords to search for, expressed as a slice of `KeyValues` using the [`keyVals`] function. + +fragments +: (`slice`) A list of special keywords that is used for indices configured as type "fragments". This will match the [fragment](g) identifiers of the documents. + +A fictional example using all of the above options: + +```go-html-template +{{ $page := . }} +{{ $opts := dict + "indices" (slice "tags" "keywords") + "document" $page + "namedSlices" (slice (keyVals "tags" "hugo" "rocks") (keyVals "date" $page.Date)) + "fragments" (slice "heading-1" "heading-2") +}} +``` + +> [!note] +> We improved and simplified this feature in Hugo 0.111.0. Before this we had 3 different methods: `Related`, `RelatedTo` and `RelatedIndices`. Now we have only one method: `Related`. The old methods are still available but deprecated. Also see [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature. + +## Index content headings + +Hugo can index the headings in your content and use this to find related content. You can enable this by adding a index of type `fragments` to your `related` configuration: + +{{< code-toggle file=hugo >}} +[related] +threshold = 20 +includeNewer = true +toLower = false +[[related.indices]] +name = "fragmentrefs" +type = "fragments" +applyFilter = true +weight = 80 +{{< /code-toggle >}} + +- The `name` maps to a optional front matter slice attribute that can be used to link from the page level down to the fragment/heading level. +- If `applyFilter` is enabled, the `.HeadingsFiltered` on each page in the result will reflect the filtered headings. This is useful if you want to show the headings in the related content listing: + +```go-html-template +{{ $related := .Site.RegularPages.Related . | first 5 }} +{{ with $related }} +

    See Also

    +
      + {{ range $i, $p := . }} +
    • + {{ .LinkTitle }} + {{ with .HeadingsFiltered }} +
        + {{ range . }} + {{ $link := printf "%s#%s" $p.RelPermalink .ID | safeURL }} +
      • + {{ .Title }} +
      • + {{ end }} +
      + {{ end }} +
    • + {{ end }} +
    +{{ end }} +``` + +## Configuration + +See [configure related content](/configuration/related-content/). + +[`keyVals`]: /functions/collections/keyvals/ diff --git a/docs/content/en/content-management/related.md b/docs/content/en/content-management/related.md deleted file mode 100644 index e80c0f06b..000000000 --- a/docs/content/en/content-management/related.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Related Content -description: List related content in "See Also" sections. -categories: [content management] -keywords: [content] -menu: - docs: - parent: content-management - weight: 110 -toc: true -weight: 110 -aliases: [/content/related/,/related/] ---- - -Hugo uses a set of factors to identify a page's related content based on Front Matter parameters. This can be tuned to the desired set of indices and parameters or left to Hugo's default [Related Content configuration](#configure-related-content). - -## List Related Content - -To list up to 5 related pages (which share the same _date_ or _keyword_ parameters) is as simple as including something similar to this partial in your single page template: - -{{< code file="layouts/partials/related.html" >}} -{{ $related := .Site.RegularPages.Related . | first 5 }} -{{ with $related }} -

    See Also

    - -{{ end }} -{{< /code >}} - -The `Related` method takes one argument which may be a `Page` or a options map. The options map have these options: - -indices -: The indices to search in. - -document -: The document to search for related content for. - -namedSlices -: The keywords to search for. - -fragments -: Fragments holds a a list of special keywords that is used for indices configured as type "fragments". This will match the fragment identifiers of the documents. - -A fictional example using all of the above options: - -```go-html-template -{{ $page := . }} -{{ $opts := - "indices" (slice "tags" "keywords") - "document" $page - "namedSlices" (slice (keyVals "tags" "hugo" "rocks") (keyVals "date" $page.Date)) - "fragments" (slice "heading-1" "heading-2") -}} -``` - -{{% note %}} -We improved and simplified this feature in Hugo 0.111.0. Before this we had 3 different methods: `Related`, `RelatedTo` and `RelatedIndicies`. Now we have only one method: `Related`. The old methods are still available but deprecated. Also see [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature. -{{% /note %}} - -## Index Content Headings in Related Content - -{{< new-in "0.111.0" >}} - -Hugo can index the headings in your content and use this to find related content. You can enable this by adding a index of type `fragments` to your `related` configuration: - -{{< code-toggle file="hugo" copy=false >}} -[related] -threshold = 20 -includeNewer = true -toLower = false -[[related.indices]] -name = "fragmentrefs" -type = "fragments" -applyFilter = false -weight = 80 -{{< /code-toggle >}} - -* The `name` maps to a optional front matter slice attribute that can be used to link from the page level down to the fragment/heading level. -* If `applyFilter`is enabled, the `.HeadingsFiltered` on each page in the result will reflect the filtered headings. This is useful if you want to show the headings in the related content listing: - -```go-html-template -{{ $related := .Site.RegularPages.Related . | first 5 }} -{{ with $related }} -

    See Also

    -
      - {{ range $i, $p := . }} -
    • - {{ .Title }} - {{ with .HeadingsFiltered }} -
        - {{ range . }} - {{ $link := printf "%s#%s" $p.RelPermalink .ID | safeURL }} -
      • - {{ .Title }} -
      • - {{ end }} -
      - {{ end }} -
    • - {{ end }} -
    -{{ end }} -``` - -## Configure Related Content - -Hugo provides a sensible default configuration of Related Content, but you can fine-tune this in your configuration, on the global or language level if needed. - -### Default configuration - -Without any `related` configuration set on the project, Hugo's Related Content methods will use the following. - -{{< code-toggle file="hugo" >}} -related: - threshold: 80 - includeNewer: false - toLower: false - indices: - - name: keywords - weight: 100 - - name: date - weight: 10 -{{< /code-toggle >}} - -Note that if you have configured `tags` as a taxonomy, `tags` will also be added to the default configuration above with the weight of `80`. - -Custom configuration should be set using the same syntax. - -{{% note %}} -If you add a `related` config section, you need to add a complete configuration. It is not possible to just set, say, `includeNewer` and use the rest from the Hugo defaults. -{{% /note %}} - -### Top Level Config Options - -threshold -: A value between 0-100. Lower value will give more, but maybe not so relevant, matches. - -includeNewer -: Set to true to include **pages newer than the current page** in the related content listing. This will mean that the output for older posts may change as new related content gets added. - -toLower -: Set to true to lower case keywords in both the indexes and the queries. This may give more accurate results at a slight performance penalty. Note that this can also be set per index. - -### Config Options per Index - -name -: The index name. This value maps directly to a page param. Hugo supports string values (`author` in the example) and lists (`tags`, `keywords` etc.) and time and date objects. - -type -: {{< new-in "0.111.0" >}}. One of `basic`(default) or `fragments`. - -applyFilter -: {{< new-in "0.111.0" >}}. Apply a `type` specific filter to the result of a search. This is currently only used for the `fragments` type. - -weight -: An integer weight that indicates _how important_ this parameter is relative to the other parameters. It can be 0, which has the effect of turning this index off, or even negative. Test with different values to see what fits your content best. - - -cardinalityThreshold (default 0) -: {{< new-in "0.111.0" >}}. A percentage (0-100) used to remove common keywords from the index. As an example, setting this to 50 will remove all keywords that are used in more than 50% of the documents in the index. - -pattern -: This is currently only relevant for dates. When listing related content, we may want to list content that is also close in time. Setting "2006" (default value for date indexes) as the pattern for a date index will add weight to pages published in the same year. For busier blogs, "200601" (year and month) may be a better default. - -toLower -: See above. - -## Performance Considerations - -**Fast is Hugo's middle name** and we would not have released this feature had it not been blistering fast. - -This feature has been in the back log and requested by many for a long time. The development got this recent kick start from this Twitter thread: - -{{< tweet user="scott_lowe" id="898398437527363585" >}} - -Scott S. Lowe removed the "Related Content" section built using the `intersect` template function on tags, and the build time dropped from 30 seconds to less than 2 seconds on his 1700 content page sized blog. - -He should now be able to add an improved version of that "Related Content" section without giving up the fast live-reloads. But it's worth noting that: - -* If you don't use any of the `Related` methods, you will not use the Relate Content feature, and performance will be the same as before. -* Calling `.RegularPages.Related` etc. will create one inverted index, also sometimes named posting list, that will be reused for any lookups in that same page collection. Doing that in addition to, as an example, calling `.Pages.Related` will work as expected, but will create one additional inverted index. This should still be very fast, but worth having in mind, especially for bigger sites. - -{{% note %}} -We currently do not index **Page content**. We thought we would release something that will make most people happy before we start solving [Sherlock's last case](https://github.com/joearms/sherlock). -{{% /note %}} diff --git a/docs/content/en/content-management/sections.md b/docs/content/en/content-management/sections.md index 10c87e6cb..f7a2296f5 100644 --- a/docs/content/en/content-management/sections.md +++ b/docs/content/en/content-management/sections.md @@ -1,96 +1,139 @@ --- -title: Content Sections -linkTitle: Sections -description: Hugo generates a **section tree** that matches your content. -categories: [content management] -keywords: [lists,sections,content types,organization] -menu: - docs: - parent: content-management - weight: 120 -toc: true -weight: 120 +title: Sections +description: Organize content into sections. + +categories: [] +keywords: [] aliases: [/content/sections/] --- -A **Section** is a collection of pages that gets defined based on the -organization structure under the `content/` directory. +## Overview -By default, all the **first-level** directories under `content/` form their own -sections (**root sections**) provided they constitute [Branch Bundles][branch bundles]. -Directories which are just [Leaf Bundles][leaf bundles] do *not* form -their own sections, despite being first-level directories. +{{% glossary-term "section" %}} -If a user needs to define a section `foo` at a deeper level, they need to create -a directory named `foo` with an `_index.md` file (see [Branch Bundles][branch bundles] -for more information). - - -{{% note %}} -A **section** cannot be defined or overridden by a front matter parameter -- it -is strictly derived from the content organization structure. -{{% /note %}} - -## Nested Sections - -The sections can be nested as deeply as you need. - -```bash -content -└── blog <-- Section, because first-level dir under content/ - ├── funny-cats - │   ├── mypost.md - │   └── kittens <-- Section, because contains _index.md - │   └── _index.md - └── tech <-- Section, because contains _index.md - └── _index.md +```text +content/ +├── articles/ <-- section (top-level directory) +│ ├── 2022/ +│ │ ├── article-1/ +│ │ │ ├── cover.jpg +│ │ │ └── index.md +│ │ └── article-2.md +│ └── 2023/ +│ ├── article-3.md +│ └── article-4.md +├── products/ <-- section (top-level directory) +│ ├── product-1/ <-- section (has _index.md file) +│ │ ├── benefits/ <-- section (has _index.md file) +│ │ │ ├── _index.md +│ │ │ ├── benefit-1.md +│ │ │ └── benefit-2.md +│ │ ├── features/ <-- section (has _index.md file) +│ │ │ ├── _index.md +│ │ │ ├── feature-1.md +│ │ │ └── feature-2.md +│ │ └── _index.md +│ └── product-2/ <-- section (has _index.md file) +│ ├── benefits/ <-- section (has _index.md file) +│ │ ├── _index.md +│ │ ├── benefit-1.md +│ │ └── benefit-2.md +│ ├── features/ <-- section (has _index.md file) +│ │ ├── _index.md +│ │ ├── feature-1.md +│ │ └── feature-2.md +│ └── _index.md +├── _index.md +└── about.md ``` -**The important part to understand is, that to make the section tree fully navigational, at least the lower-most section needs a content file. (e.g. `_index.md`).** +The example above has two top-level sections: articles and products. None of the directories under articles are sections, while all of the directories under products are sections. A section within a section is a known as a nested section or subsection. -{{% note %}} -When we talk about a **section** in correlation with template selection, it is -currently always the *root section* only (`/blog/funny-cats/mypost/ => blog`). +## Explanation -If you need a specific template for a sub-section, you need to adjust either the `type` or `layout` in front matter. -{{% /note %}} +Sections and non-sections behave differently. -## Example: Breadcrumb Navigation +||Sections|Non-sections +:--|:-:|:-: +Directory names become URL segments|:heavy_check_mark:|:heavy_check_mark: +Have logical ancestors and descendants|:heavy_check_mark:|:x: +Have list pages|:heavy_check_mark:|:x: -With the available [section variables and methods](#section-page-variables-and-methods) you can build powerful navigation. One common example would be a partial to show Breadcrumb navigation: +With the file structure from the [example above](#overview): -{{< code file="layouts/partials/breadcrumb.html" >}} -